command: deal with plan states

This commit is contained in:
Mitchell Hashimoto 2015-02-21 18:00:08 -08:00
parent 89d3a10adf
commit 4ec63bc2ef
3 changed files with 105 additions and 41 deletions

View File

@ -5,7 +5,6 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"path/filepath" "path/filepath"
@ -41,11 +40,6 @@ type Meta struct {
color bool color bool
oldUi cli.Ui oldUi cli.Ui
// useRemoteState is enabled if we are using remote state storage
// This is set when the context is loaded if we read from a remote
// enabled state file.
useRemoteState bool
// statePath is the path to the state file. If this is empty, then // statePath is the path to the state file. If this is empty, then
// no state will be loaded. It is also okay for this to be a path to // no state will be loaded. It is also okay for this to be a path to
// a file that doesn't exist; it is assumed that this means that there // a file that doesn't exist; it is assumed that this means that there
@ -101,14 +95,16 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
plan, err := terraform.ReadPlan(f) plan, err := terraform.ReadPlan(f)
f.Close() f.Close()
if err == nil { if err == nil {
// Check if remote state is enabled, but do not refresh. // Setup our state
// Since a plan is supposed to lock-in the changes, we do not state, statePath, err := StateFromPlan(m.statePath, plan)
// attempt a state refresh. if err != nil {
if plan != nil && plan.State != nil && plan.State.Remote != nil && plan.State.Remote.Type != "" { return nil, false, fmt.Errorf("Error loading plan: %s", err)
log.Printf("[INFO] Enabling remote state from plan")
m.useRemoteState = true
} }
// Set our state
m.state = state
m.stateOutPath = statePath
if len(m.variables) > 0 { if len(m.variables) > 0 {
return nil, false, fmt.Errorf( return nil, false, fmt.Errorf(
"You can't set variables with the '-var' or '-var-file' flag\n" + "You can't set variables with the '-var' or '-var-file' flag\n" +

View File

@ -8,6 +8,7 @@ import (
"github.com/hashicorp/errwrap" "github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote" "github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
) )
// State returns the proper state.State implementation to represent the // State returns the proper state.State implementation to represent the
@ -24,7 +25,7 @@ func State(localPath string) (state.State, string, error) {
remoteCachePath := filepath.Join(DefaultDataDir, DefaultStateFilename) remoteCachePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
if _, err := os.Stat(remoteCachePath); err == nil { if _, err := os.Stat(remoteCachePath); err == nil {
// We have a remote state, initialize that. // We have a remote state, initialize that.
result, err = remoteState(remoteCachePath) result, err = remoteStateFromPath(remoteCachePath)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
@ -62,29 +63,65 @@ func State(localPath string) (state.State, string, error) {
resultPath = localPath resultPath = localPath
} }
// If we have a result, make sure to back it up
if result != nil {
result = &state.BackupState{
Real: result,
Path: resultPath + DefaultBackupExtention,
}
}
// Return whatever state we have // Return whatever state we have
return result, resultPath, nil return result, resultPath, nil
} }
func remoteState(path string) (state.State, error) { // StateFromPlan gets our state from the plan.
// First create the local state for the path func StateFromPlan(
local := &state.LocalState{Path: path} localPath string, plan *terraform.Plan) (state.State, string, error) {
if err := local.RefreshState(); err != nil { var result state.State
return nil, err resultPath := localPath
} if plan != nil && plan.State != nil &&
localState := local.State() plan.State.Remote != nil && plan.State.Remote.Type != "" {
var err error
// It looks like we have a remote state in the plan, so
// we have to initialize that.
resultPath = filepath.Join(DefaultDataDir, DefaultStateFilename)
result, err = remoteState(plan.State, resultPath, false)
if err != nil {
return nil, "", err
}
}
if result == nil {
local := &state.LocalState{Path: resultPath}
local.SetState(plan.State)
result = local
}
// If we have a result, make sure to back it up
result = &state.BackupState{
Real: result,
Path: resultPath + DefaultBackupExtention,
}
return result, resultPath, nil
}
func remoteState(
local *terraform.State,
localPath string, refresh bool) (state.State, error) {
// If there is no remote settings, it is an error // If there is no remote settings, it is an error
if localState.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")
} }
// Initialize the remote client based on the local state // Initialize the remote client based on the local state
client, err := remote.NewClient(localState.Remote.Type, localState.Remote.Config) client, err := remote.NewClient(local.Remote.Type, local.Remote.Config)
if err != nil { if err != nil {
return nil, errwrap.Wrapf(fmt.Sprintf( return nil, errwrap.Wrapf(fmt.Sprintf(
"Error initializing remote driver '%s': {{err}}", "Error initializing remote driver '%s': {{err}}",
localState.Remote.Type), err) local.Remote.Type), err)
} }
// Create the remote client // Create the remote client
@ -92,10 +129,11 @@ func remoteState(path string) (state.State, error) {
// Create the cached client // Create the cached client
cache := &state.CacheState{ cache := &state.CacheState{
Cache: local, Cache: &state.LocalState{Path: localPath},
Durable: durable, Durable: durable,
} }
if refresh {
// Refresh the cache // Refresh the cache
if err := cache.RefreshState(); err != nil { if err := cache.RefreshState(); err != nil {
return nil, errwrap.Wrapf( return nil, errwrap.Wrapf(
@ -107,15 +145,30 @@ func remoteState(path string) (state.State, error) {
case state.CacheRefreshLocalNewer: case state.CacheRefreshLocalNewer:
case state.CacheRefreshUpdateLocal: case state.CacheRefreshUpdateLocal:
// Write our local state out to the durable storage to start. // Write our local state out to the durable storage to start.
if err := cache.WriteState(localState); err != nil { if err := cache.WriteState(local); err != nil {
return nil, errwrap.Wrapf("Error preparing remote state: {{err}}", err) return nil, errwrap.Wrapf(
"Error preparing remote state: {{err}}", err)
} }
if err := cache.PersistState(); err != nil { if err := cache.PersistState(); err != nil {
return nil, errwrap.Wrapf("Error preparing remote state: {{err}}", err) return nil, errwrap.Wrapf(
"Error preparing remote state: {{err}}", err)
} }
default: default:
return nil, errwrap.Wrapf("Error initilizing remote state: {{err}}", err) return nil, errwrap.Wrapf(
"Error initilizing remote state: {{err}}", err)
}
} }
return cache, nil return cache, nil
} }
func remoteStateFromPath(path string) (state.State, error) {
// First create the local state for the path
local := &state.LocalState{Path: path}
if err := local.RefreshState(); err != nil {
return nil, err
}
localState := local.State()
return remoteState(localState, path, true)
}

View File

@ -18,6 +18,11 @@ type LocalState struct {
written bool written bool
} }
// SetState will force a specific state in-memory for this local state.
func (s *LocalState) SetState(state *terraform.State) {
s.state = state
}
// StateReader impl. // StateReader impl.
func (s *LocalState) State() *terraform.State { func (s *LocalState) State() *terraform.State {
return s.state return s.state
@ -34,6 +39,16 @@ func (s *LocalState) WriteState(state *terraform.State) error {
path = s.Path path = s.Path
} }
// If we don't have any state, we actually delete the file if it exists
if state == nil {
err := os.Remove(path)
if err != nil && os.IsNotExist(err) {
return nil
}
return err
}
f, err := os.Create(path) f, err := os.Create(path)
if err != nil { if err != nil {
return err return err