diff --git a/command/meta.go b/command/meta.go index b009a9178..5f3e9ad11 100644 --- a/command/meta.go +++ b/command/meta.go @@ -5,7 +5,6 @@ import ( "flag" "fmt" "io" - "log" "os" "path/filepath" @@ -41,11 +40,6 @@ type Meta struct { color bool 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 // 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 @@ -101,14 +95,16 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { plan, err := terraform.ReadPlan(f) f.Close() if err == nil { - // Check if remote state is enabled, but do not refresh. - // Since a plan is supposed to lock-in the changes, we do not - // attempt a state refresh. - if plan != nil && plan.State != nil && plan.State.Remote != nil && plan.State.Remote.Type != "" { - log.Printf("[INFO] Enabling remote state from plan") - m.useRemoteState = true + // Setup our state + state, statePath, err := StateFromPlan(m.statePath, plan) + if err != nil { + return nil, false, fmt.Errorf("Error loading plan: %s", err) } + // Set our state + m.state = state + m.stateOutPath = statePath + if len(m.variables) > 0 { return nil, false, fmt.Errorf( "You can't set variables with the '-var' or '-var-file' flag\n" + diff --git a/command/state.go b/command/state.go index 045c4745f..5e5dc566a 100644 --- a/command/state.go +++ b/command/state.go @@ -8,6 +8,7 @@ import ( "github.com/hashicorp/errwrap" "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/state/remote" + "github.com/hashicorp/terraform/terraform" ) // 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) if _, err := os.Stat(remoteCachePath); err == nil { // We have a remote state, initialize that. - result, err = remoteState(remoteCachePath) + result, err = remoteStateFromPath(remoteCachePath) if err != nil { return nil, "", err } @@ -62,29 +63,65 @@ func State(localPath string) (state.State, string, error) { 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 result, resultPath, nil } -func remoteState(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() +// StateFromPlan gets our state from the plan. +func StateFromPlan( + localPath string, plan *terraform.Plan) (state.State, string, error) { + var result state.State + resultPath := localPath + if plan != nil && plan.State != nil && + 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 localState.Remote == nil { + if local.Remote == nil { return nil, fmt.Errorf("Remote state cache has no remote info") } // 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 { return nil, errwrap.Wrapf(fmt.Sprintf( "Error initializing remote driver '%s': {{err}}", - localState.Remote.Type), err) + local.Remote.Type), err) } // Create the remote client @@ -92,30 +129,46 @@ func remoteState(path string) (state.State, error) { // Create the cached client cache := &state.CacheState{ - Cache: local, + Cache: &state.LocalState{Path: localPath}, Durable: durable, } - // Refresh the cache - if err := cache.RefreshState(); err != nil { - return nil, errwrap.Wrapf( - "Error reloading remote state: {{err}}", err) - } - switch cache.RefreshResult() { - case state.CacheRefreshNoop: - case state.CacheRefreshInit: - case state.CacheRefreshLocalNewer: - case state.CacheRefreshUpdateLocal: - // Write our local state out to the durable storage to start. - if err := cache.WriteState(localState); err != nil { - return nil, errwrap.Wrapf("Error preparing remote state: {{err}}", err) + if refresh { + // Refresh the cache + if err := cache.RefreshState(); err != nil { + return nil, errwrap.Wrapf( + "Error reloading remote state: {{err}}", err) } - if err := cache.PersistState(); err != nil { - return nil, errwrap.Wrapf("Error preparing remote state: {{err}}", err) + switch cache.RefreshResult() { + case state.CacheRefreshNoop: + case state.CacheRefreshInit: + case state.CacheRefreshLocalNewer: + case state.CacheRefreshUpdateLocal: + // Write our local state out to the durable storage to start. + if err := cache.WriteState(local); err != nil { + return nil, errwrap.Wrapf( + "Error preparing remote state: {{err}}", err) + } + if err := cache.PersistState(); err != nil { + return nil, errwrap.Wrapf( + "Error preparing remote state: {{err}}", err) + } + default: + return nil, errwrap.Wrapf( + "Error initilizing remote state: {{err}}", err) } - default: - return nil, errwrap.Wrapf("Error initilizing remote state: {{err}}", err) } 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) +} diff --git a/state/local.go b/state/local.go index 22c7a7bd0..355b9b53c 100644 --- a/state/local.go +++ b/state/local.go @@ -18,6 +18,11 @@ type LocalState struct { 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. func (s *LocalState) State() *terraform.State { return s.state @@ -34,6 +39,16 @@ func (s *LocalState) WriteState(state *terraform.State) error { 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) if err != nil { return err