From 2c2b560d7fc7155a20da26e165941023ff49283d Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Mon, 23 Feb 2015 10:20:40 -0800 Subject: [PATCH] command/remote: no more remote package --- command/meta.go | 47 ++++++++---- command/remote.go | 166 ++++++++++++++++++++--------------------- command/remote_test.go | 25 +++---- command/state.go | 122 +++++++++++++++++++++--------- 4 files changed, 213 insertions(+), 147 deletions(-) diff --git a/command/meta.go b/command/meta.go index dc48afe9d..d6dc0ff8c 100644 --- a/command/meta.go +++ b/command/meta.go @@ -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. diff --git a/command/remote.go b/command/remote.go index 6567d8037..f8f8cffb3 100644 --- a/command/remote.go +++ b/command/remote.go @@ -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", diff --git a/command/remote_test.go b/command/remote_test.go index 434fda3a8..54c31f9ed 100644 --- a/command/remote_test.go +++ b/command/remote_test.go @@ -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") } diff --git a/command/state.go b/command/state.go index ecacfdd9a..9fceb61e7 100644 --- a/command/state.go +++ b/command/state.go @@ -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) }