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

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

View File

@ -1,15 +1,14 @@
package command package command
import ( import (
"bytes"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"strings" "strings"
"github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -55,6 +54,9 @@ func (c *RemoteCommand) Run(args []string) int {
return 1 return 1
} }
// Set the local state path
c.statePath = c.conf.statePath
// Populate the various configurations // Populate the various configurations
c.remoteConf.Config = map[string]string{ c.remoteConf.Config = map[string]string{
"address": address, "address": address,
@ -63,50 +65,56 @@ func (c *RemoteCommand) Run(args []string) int {
"path": path, "path": path,
} }
// Check if have an existing local state file // Get the state information. We specifically request the cache only
haveLocal, err := remote.HaveLocalState() // for the remote state here because it is possible the remote state
if err != nil { // is invalid and we don't want to error.
c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err)) 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 return 1
} }
// Check if we have the non-managed state file // Get the local and remote [cached] state
haveNonManaged, err := remote.ExistsFile(c.conf.statePath) localState := c.stateResult.Local.State()
if err != nil { var remoteState *terraform.State
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err)) if remote := c.stateResult.Remote; remote != nil {
return 1 remoteState = remote.State()
} }
// Check if remote state is being disabled // Check if remote state is being disabled
if c.conf.disableRemote { if c.conf.disableRemote {
if !haveLocal { if !remoteState.IsRemote() {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting.")) c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1 return 1
} }
if haveNonManaged { if !localState.Empty() {
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.", c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath)) c.conf.statePath))
return 1 return 1
} }
return c.disableRemoteState() return c.disableRemoteState()
} }
// Ensure there is no conflict // Ensure there is no conflict
haveCache := !remoteState.Empty()
haveLocal := !localState.Empty()
switch { 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.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath)) c.conf.statePath))
return 1 return 1
case !haveLocal && !haveNonManaged: case !haveCache && !haveLocal:
// If we don't have either state file, initialize a blank state file // If we don't have either state file, initialize a blank state file
return c.initBlankState() return c.initBlankState()
case haveLocal && !haveNonManaged: case haveCache && !haveLocal:
// Update the remote state target potentially // Update the remote state target potentially
return c.updateRemoteConfig() return c.updateRemoteConfig()
case !haveLocal && haveNonManaged: case !haveCache && haveLocal:
// Enable remote state management // Enable remote state management
return c.enableRemoteState() return c.enableRemoteState()
} }
@ -117,71 +125,66 @@ func (c *RemoteCommand) Run(args []string) int {
// disableRemoteState is used to disable remote state management, // disableRemoteState is used to disable remote state management,
// and move the state file into place. // and move the state file into place.
func (c *RemoteCommand) disableRemoteState() int { func (c *RemoteCommand) disableRemoteState() int {
// Get the local state if c.stateResult == nil {
local, _, err := remote.ReadLocalState() c.Ui.Error(fmt.Sprintf(
if err != nil { "Internal error. State() must be called internally before remote\n" +
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err)) "state can be disabled. Please report this as a bug."))
return 1 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 // Ensure we have the latest state before disabling
if c.conf.pullOnDisable { if c.conf.pullOnDisable {
log.Printf("[INFO] Refreshing local state from remote server") log.Printf("[INFO] Refreshing local state from remote server")
change, err := remote.RefreshState(local.Remote) if err := remote.RefreshState(); err != nil {
if err != nil {
c.Ui.Error(fmt.Sprintf( c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %v", err)) "Failed to refresh from remote state: %s", err))
return 1 return 1
} }
// Exit if we were unable to update // Exit if we were unable to update
if !change.SuccessfulPull() { if change := remote.RefreshResult(); !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change)) c.Ui.Error(fmt.Sprintf("%s", change))
return 1 return 1
} else { } else {
log.Printf("[INFO] %s", change) 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 // Clear the remote management, and copy into place
local.Remote = nil newState := remote.State()
fh, err := os.Create(c.conf.statePath) newState.Remote = nil
if err != nil { if err := local.WriteState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to create state file '%s': %v", c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err)) c.conf.statePath, err))
return 1 return 1
} }
defer fh.Close() if err := local.PersistState(); err != nil {
if err := terraform.WriteState(local, fh); err != nil { c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %v",
c.conf.statePath, err)) c.conf.statePath, err))
return 1 return 1
} }
// Remove the old state file // Remove the old state file
path, err := remote.HiddenStatePath() if err := os.Remove(c.stateResult.RemotePath); err != nil {
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)) c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1 return 1
} }
return 0 return 0
} }
// validateRemoteConfig is used to verify that the remote configuration // validateRemoteConfig is used to verify that the remote configuration
// we have is valid // we have is valid
func (c *RemoteCommand) validateRemoteConfig() error { func (c *RemoteCommand) validateRemoteConfig() error {
err := remote.ValidConfig(&c.remoteConf) conf := c.remoteConf
_, err := remote.NewClient(conf.Type, conf.Config)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err)) c.Ui.Error(fmt.Sprintf("%s", err))
} }
@ -196,18 +199,17 @@ func (c *RemoteCommand) initBlankState() int {
return 1 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 // Make a blank state, attach the remote configuration
blank := terraform.NewState() blank := terraform.NewState()
blank.Remote = &c.remoteConf blank.Remote = &c.remoteConf
// Persist the state // 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)) c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1 return 1
} }
@ -225,16 +227,17 @@ func (c *RemoteCommand) updateRemoteConfig() int {
return 1 return 1
} }
// Read in the local state // Read in the local state, which is just the cache of the remote state
local, _, err := remote.ReadLocalState() remote := c.stateResult.Remote.Cache
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
return 1
}
// Update the configuration // Update the configuration
local.Remote = &c.remoteConf state := remote.State()
if err := remote.PersistState(local); err != nil { 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)) c.Ui.Error(fmt.Sprintf("%s", err))
return 1 return 1
} }
@ -252,21 +255,10 @@ func (c *RemoteCommand) enableRemoteState() int {
return 1 return 1
} }
// Make the hidden directory // Read the local state
if err := remote.EnsureDirectory(); err != nil { local := c.stateResult.Local
c.Ui.Error(fmt.Sprintf("%s", err)) if err := local.RefreshState(); err != nil {
return 1 c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
}
// 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 return 1
} }
@ -279,25 +271,31 @@ func (c *RemoteCommand) enableRemoteState() int {
} }
log.Printf("[INFO] Writing backup state to: %s", backupPath) log.Printf("[INFO] Writing backup state to: %s", backupPath)
f, err := os.Create(backupPath) backup := &state.LocalState{Path: backupPath}
if err == nil { if err := backup.WriteState(local.State()); err != nil {
err = terraform.WriteState(state, f) c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
f.Close() return 1
} }
if err != nil { if err := backup.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1 return 1
} }
} }
// Update the local configuration, move into place // Update the local configuration, move into place
state := local.State()
state.Remote = &c.remoteConf 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)) c.Ui.Error(fmt.Sprintf("%s", err))
return 1 return 1
} }
// Remove the state file // Remove the original, local state file
log.Printf("[INFO] Removing state file: %s", c.conf.statePath) log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
if err := os.Remove(c.conf.statePath); err != nil { if err := os.Remove(c.conf.statePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v", c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",

View File

@ -4,9 +4,11 @@ import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"testing" "testing"
"github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -71,11 +73,6 @@ func TestRemote_disable(t *testing.T) {
} }
// Ensure we updated // 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 { if newState.Remote != nil {
t.Fatalf("remote configuration not removed") t.Fatalf("remote configuration not removed")
} }
@ -96,11 +93,15 @@ func TestRemote_disable_noPull(t *testing.T) {
s = terraform.NewState() s = terraform.NewState()
s.Serial = 5 s.Serial = 5
s.Remote = conf 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 { if err := state.PersistState(); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %s", err)
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
@ -140,12 +141,6 @@ func TestRemote_disable_noPull(t *testing.T) {
t.Fatalf("err: %v", err) 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 { if newState.Remote != nil {
t.Fatalf("remote configuration not removed") t.Fatalf("remote configuration not removed")
} }

View File

@ -21,7 +21,12 @@ type StateOpts struct {
LocalPathOut string LocalPathOut string
// RemotePath is the path where the remote state cache would be. // RemotePath is the path where the remote state cache would be.
//
// RemoteCache, if true, will set the result to only be the cache
// and not backed by any real durable storage.
RemotePath string RemotePath string
RemoteCacheOnly bool
RemoteRefresh bool
// BackupPath is the path where the backup will be placed. If not set, // 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 // it is assumed to be the path where the state is stored locally
@ -29,25 +34,74 @@ type StateOpts struct {
BackupPath string 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 // State returns the proper state.State implementation to represent the
// current environment. // current environment.
// //
// localPath is the path to where state would be if stored locally. // 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 // dataDir is the path to the local data directory where the remote state
// cache would be stored. // cache would be stored.
func State(opts *StateOpts) (state.State, string, error) { func State(opts *StateOpts) (*StateResult, error) {
var result state.State result := new(StateResult)
var resultPath string
// Get the remote state cache path // Get the remote state cache path
if opts.RemotePath != "" { if opts.RemotePath != "" {
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
}
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 { if _, err := os.Stat(opts.RemotePath); err == nil {
// We have a remote state, initialize that. // We have a remote state, initialize that.
result, err = remoteStateFromPath(opts.RemotePath) remote, err = remoteStateFromPath(
opts.RemotePath,
opts.RemoteRefresh)
if err != nil { if err != nil {
return nil, "", err return nil, err
} }
resultPath = opts.RemotePath }
}
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, Path: opts.LocalPath,
PathOut: opts.LocalPathOut, 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() err := local.RefreshState()
if err != nil { if err == nil {
isNotExist := false if result.State != nil && !result.State.State().Empty() {
errwrap.Walk(err, func(e error) {
if !isNotExist && os.IsNotExist(e) {
isNotExist = true
}
})
if isNotExist {
err = nil
}
} else {
if result != nil {
if !local.State().Empty() { if !local.State().Empty() {
// We already have a remote state... that is an error. // 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.", "Remote state found, but state file '%s' also present.",
opts.LocalPath) opts.LocalPath)
} }
@ -82,34 +134,34 @@ func State(opts *StateOpts) (state.State, string, error) {
} }
} }
if err != nil { if err != nil {
return nil, "", errwrap.Wrapf( return nil, errwrap.Wrapf(
"Error reading local state: {{err}}", err) "Error reading local state: {{err}}", err)
} }
if local != nil { if local != nil {
result = local result.State = local
resultPath = opts.LocalPath result.StatePath = opts.LocalPath
if opts.LocalPathOut != "" { if opts.LocalPathOut != "" {
resultPath = opts.LocalPathOut result.StatePath = opts.LocalPathOut
} }
} }
} }
// If we have a result, make sure to back it up // If we have a result, make sure to back it up
if result != nil { if result.State != nil {
backupPath := resultPath + DefaultBackupExtention backupPath := result.StatePath + DefaultBackupExtention
if opts.BackupPath != "" { if opts.BackupPath != "" {
backupPath = opts.BackupPath backupPath = opts.BackupPath
} }
result = &state.BackupState{ result.State = &state.BackupState{
Real: result, Real: result.State,
Path: backupPath, Path: backupPath,
} }
} }
// Return whatever state we have // Return whatever state we have
return result, resultPath, nil return result, nil
} }
// StateFromPlan gets our state from the plan. // StateFromPlan gets our state from the plan.
@ -147,7 +199,7 @@ func StateFromPlan(
func remoteState( func remoteState(
local *terraform.State, 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 there is no remote settings, it is an error
if local.Remote == nil { if local.Remote == nil {
return nil, fmt.Errorf("Remote state cache has no remote info") return nil, fmt.Errorf("Remote state cache has no remote info")
@ -199,7 +251,7 @@ func remoteState(
return cache, nil 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 // First create the local state for the path
local := &state.LocalState{Path: path} local := &state.LocalState{Path: path}
if err := local.RefreshState(); err != nil { if err := local.RefreshState(); err != nil {
@ -207,5 +259,5 @@ func remoteStateFromPath(path string) (state.State, error) {
} }
localState := local.State() localState := local.State()
return remoteState(localState, path, true) return remoteState(localState, path, refresh)
} }