diff --git a/command/command.go b/command/command.go index 89ada2e37..7779a64bf 100644 --- a/command/command.go +++ b/command/command.go @@ -10,6 +10,9 @@ import ( // Set to true when we're testing var test bool = false +// DefaultDataDir is the default directory for storing local data. +const DefaultDataDir = ".terraform" + // DefaultStateFilename is the default filename used for the state file. const DefaultStateFilename = "terraform.tfstate" diff --git a/command/meta.go b/command/meta.go index e22f61bce..b009a9178 100644 --- a/command/meta.go +++ b/command/meta.go @@ -10,7 +10,7 @@ import ( "path/filepath" "github.com/hashicorp/terraform/config/module" - "github.com/hashicorp/terraform/remote" + "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" "github.com/mitchellh/cli" "github.com/mitchellh/colorstring" @@ -24,7 +24,7 @@ type Meta struct { // State read when calling `Context`. This is available after calling // `Context`. - state *terraform.State + state state.State // This can be set by the command itself to provide extra hooks. extraHooks []terraform.Hook @@ -78,11 +78,6 @@ func (m *Meta) initStatePaths() { // StateOutPath returns the true output path for the state file func (m *Meta) StateOutPath() string { - m.initStatePaths() - if m.useRemoteState { - path, _ := remote.HiddenStatePath() - return path - } return m.stateOutPath } @@ -132,11 +127,12 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { } // Store the loaded state - state, err := m.loadState() + state, statePath, err := State(m.statePath) if err != nil { return nil, false, err } m.state = state + m.stateOutPath = statePath // Load the root module mod, err := module.NewTreeModule("", copts.Path) @@ -154,7 +150,7 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) { } opts.Module = mod - opts.State = state + opts.State = state.State() ctx := terraform.NewContext(opts) return ctx, false, nil } @@ -175,6 +171,21 @@ func (m *Meta) InputMode() terraform.InputMode { return mode } +// State returns the state for this meta. +func (m *Meta) State() (state.State, error) { + if m.state != nil { + return m.state, nil + } + + state, _, err := State(m.statePath) + if err != nil { + return nil, err + } + + m.state = state + return state, nil +} + // UIInput returns a UIInput object to be used for asking for input. func (m *Meta) UIInput() terraform.UIInput { return &UIInput{ @@ -182,115 +193,14 @@ func (m *Meta) UIInput() terraform.UIInput { } } -// laodState is used to load the Terraform state. We give precedence -// to a remote state if enabled, and then check the normal state path. -func (m *Meta) loadState() (*terraform.State, error) { - // Check if we remote state is enabled - localCache, _, err := remote.ReadLocalState() - if err != nil { - return nil, fmt.Errorf("Error loading state: %s", err) - } - - // Set the state if enabled - var state *terraform.State - if localCache != nil { - // Refresh the state - log.Printf("[INFO] Refreshing local state...") - changes, err := remote.RefreshState(localCache.Remote) - if err != nil { - return nil, fmt.Errorf("Failed to refresh state: %v", err) - } - switch changes { - case remote.StateChangeNoop: - case remote.StateChangeInit: - case remote.StateChangeLocalNewer: - case remote.StateChangeUpdateLocal: - // Reload the state since we've udpated - localCache, _, err = remote.ReadLocalState() - if err != nil { - return nil, fmt.Errorf("Error loading state: %s", err) - } - default: - return nil, fmt.Errorf("%s", changes) - } - - state = localCache - m.useRemoteState = true - } - - // Load up the state - if m.statePath != "" { - f, err := os.Open(m.statePath) - if err != nil && os.IsNotExist(err) { - // If the state file doesn't exist, it is okay, since it - // is probably a new infrastructure. - err = nil - } else if m.useRemoteState && err == nil { - err = fmt.Errorf("Remote state enabled, but state file '%s' also present.", m.statePath) - f.Close() - } else if err == nil { - state, err = terraform.ReadState(f) - f.Close() - } - if err != nil { - return nil, fmt.Errorf("Error loading state: %s", err) - } - } - return state, nil -} - // PersistState is used to write out the state, handling backup of // the existing state file and respecting path configurations. func (m *Meta) PersistState(s *terraform.State) error { - if m.useRemoteState { - return m.persistRemoteState(s) - } - return m.persistLocalState(s) -} - -// persistRemoteState is used to handle persisting a state file -// when remote state management is enabled -func (m *Meta) persistRemoteState(s *terraform.State) error { - log.Printf("[INFO] Persisting state to local cache") - if err := remote.PersistState(s); err != nil { + if err := m.state.WriteState(s); err != nil { return err } - log.Printf("[INFO] Uploading state to remote store") - change, err := remote.PushState(s.Remote, false) - if err != nil { - return err - } - if !change.SuccessfulPush() { - return fmt.Errorf("Failed to upload state: %s", change) - } - return nil -} -// persistLocalState is used to handle persisting a state file -// when remote state management is disabled. -func (m *Meta) persistLocalState(s *terraform.State) error { - m.initStatePaths() - - // Create a backup of the state before updating - if m.backupPath != "-" { - log.Printf("[INFO] Writing backup state to: %s", m.backupPath) - if err := remote.CopyFile(m.statePath, m.backupPath); err != nil { - return fmt.Errorf("Failed to backup state: %v", err) - } - } - - // Open the new state file - fh, err := os.Create(m.stateOutPath) - if err != nil { - return fmt.Errorf("Failed to open state file: %v", err) - } - defer fh.Close() - - // Write out the state - if err := terraform.WriteState(s, fh); err != nil { - return fmt.Errorf("Failed to encode the state: %v", err) - } - return nil + return m.state.PersistState() } // Input returns true if we should ask for input for context. diff --git a/command/meta_test.go b/command/meta_test.go index c23923ab1..4b1ae03f8 100644 --- a/command/meta_test.go +++ b/command/meta_test.go @@ -7,7 +7,6 @@ import ( "reflect" "testing" - "github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/terraform" ) @@ -182,156 +181,3 @@ func TestMeta_initStatePaths(t *testing.T) { t.Fatalf("bad: %#v", m) } } - -func TestMeta_persistLocal(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - m := new(Meta) - s := terraform.NewState() - if err := m.persistLocalState(s); err != nil { - t.Fatalf("err: %v", err) - } - - exists, err := remote.ExistsFile(m.stateOutPath) - if err != nil { - t.Fatalf("err: %v", err) - } - if !exists { - t.Fatalf("state should exist") - } - - // Write again, shoudl backup - if err := m.persistLocalState(s); err != nil { - t.Fatalf("err: %v", err) - } - - exists, err = remote.ExistsFile(m.backupPath) - if err != nil { - t.Fatalf("err: %v", err) - } - if !exists { - t.Fatalf("backup should exist") - } -} - -func TestMeta_persistRemote(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - err := remote.EnsureDirectory() - if err != nil { - t.Fatalf("err: %v", err) - } - - s := terraform.NewState() - conf, srv := testRemoteState(t, s, 200) - s.Remote = conf - defer srv.Close() - - m := new(Meta) - if err := m.persistRemoteState(s); err != nil { - t.Fatalf("err: %v", err) - } - - local, _, err := remote.ReadLocalState() - if err != nil { - t.Fatalf("err: %v", err) - } - if local == nil { - t.Fatalf("state should exist") - } - - if err := m.persistRemoteState(s); err != nil { - t.Fatalf("err: %v", err) - } - - backup := remote.LocalDirectory + "/" + remote.BackupHiddenStateFile - exists, err := remote.ExistsFile(backup) - if err != nil { - t.Fatalf("err: %v", err) - } - if !exists { - t.Fatalf("backup should exist") - } -} - -func TestMeta_loadState_remote(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - err := remote.EnsureDirectory() - if err != nil { - t.Fatalf("err: %v", err) - } - - s := terraform.NewState() - s.Serial = 1000 - conf, srv := testRemoteState(t, s, 200) - s.Remote = conf - defer srv.Close() - - s.Serial = 500 - if err := remote.PersistState(s); err != nil { - t.Fatalf("err: %v", err) - } - - m := new(Meta) - s1, err := m.loadState() - if err != nil { - t.Fatalf("err: %v", err) - } - if s1.Serial < 1000 { - t.Fatalf("Bad: %#v", s1) - } - - if !m.useRemoteState { - t.Fatalf("should enable remote") - } -} - -func TestMeta_loadState_statePath(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - m := new(Meta) - - s := terraform.NewState() - s.Serial = 1000 - if err := m.persistLocalState(s); err != nil { - t.Fatalf("err: %v", err) - } - - s1, err := m.loadState() - if err != nil { - t.Fatalf("err: %v", err) - } - if s1.Serial < 1000 { - t.Fatalf("Bad: %#v", s1) - } -} - -func TestMeta_loadState_conflict(t *testing.T) { - tmp, cwd := testCwd(t) - defer testFixCwd(t, tmp, cwd) - - err := remote.EnsureDirectory() - if err != nil { - t.Fatalf("err: %v", err) - } - - m := new(Meta) - - s := terraform.NewState() - if err := remote.PersistState(s); err != nil { - t.Fatalf("err: %v", err) - } - if err := m.persistLocalState(s); err != nil { - t.Fatalf("err: %v", err) - } - - _, err = m.loadState() - if err == nil { - t.Fatalf("should error with conflict") - } -} diff --git a/command/output.go b/command/output.go index 32d49de69..05fd31f34 100644 --- a/command/output.go +++ b/command/output.go @@ -32,12 +32,13 @@ func (c *OutputCommand) Run(args []string) int { } name := args[0] - state, err := c.Meta.loadState() + stateStore, err := c.Meta.State() if err != nil { c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) return 1 } + state := stateStore.State() if len(state.RootModule().Outputs) == 0 { c.Ui.Error(fmt.Sprintf( "The state file has no outputs defined. Define an output\n" + diff --git a/command/show.go b/command/show.go index a1c7471ec..c4e095a42 100644 --- a/command/show.go +++ b/command/show.go @@ -6,6 +6,7 @@ import ( "os" "strings" + statelib "github.com/hashicorp/terraform/state" "github.com/hashicorp/terraform/terraform" ) @@ -36,7 +37,7 @@ func (c *ShowCommand) Run(args []string) int { return 1 } - var err, planErr, stateErr error + var planErr, stateErr error var path string var plan *terraform.Plan var state *terraform.State @@ -68,12 +69,13 @@ func (c *ShowCommand) Run(args []string) int { } else { // We should use the default state if it exists. - c.Meta.statePath = DefaultStateFilename - state, err = c.Meta.loadState() - if err != nil { + stateStore := &statelib.LocalState{Path: DefaultStateFilename} + if err := stateStore.RefreshState(); err != nil { c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) return 1 } + + state = stateStore.State() if state == nil { c.Ui.Output("No state.") return 0 diff --git a/command/state.go b/command/state.go new file mode 100644 index 000000000..045c4745f --- /dev/null +++ b/command/state.go @@ -0,0 +1,121 @@ +package command + +import ( + "fmt" + "os" + "path/filepath" + + "github.com/hashicorp/errwrap" + "github.com/hashicorp/terraform/state" + "github.com/hashicorp/terraform/state/remote" +) + +// 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(localPath string) (state.State, string, error) { + var result state.State + var resultPath string + + // Get the remote state cache path + remoteCachePath := filepath.Join(DefaultDataDir, DefaultStateFilename) + if _, err := os.Stat(remoteCachePath); err == nil { + // We have a remote state, initialize that. + result, err = remoteState(remoteCachePath) + if err != nil { + return nil, "", err + } + resultPath = remoteCachePath + } + + // Do we have a local state? + if localPath != "" { + local := &state.LocalState{Path: localPath} + 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 { + // We already have a remote state... that is an error. + return nil, "", fmt.Errorf( + "Remote state found, but state file '%s' also present.", + localPath) + } + } + if err != nil { + return nil, "", errwrap.Wrapf( + "Error reading local state: {{err}}", err) + } + + result = local + resultPath = localPath + } + + // 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() + + // If there is no remote settings, it is an error + if localState.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) + if err != nil { + return nil, errwrap.Wrapf(fmt.Sprintf( + "Error initializing remote driver '%s': {{err}}", + localState.Remote.Type), err) + } + + // Create the remote client + durable := &remote.State{Client: client} + + // Create the cached client + cache := &state.CacheState{ + Cache: local, + 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 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) + } + + return cache, nil +} diff --git a/commands.go b/commands.go index 47aafb557..aa0fc5e4c 100644 --- a/commands.go +++ b/commands.go @@ -14,8 +14,10 @@ var Commands map[string]cli.CommandFactory // Ui is the cli.Ui used for communicating to the outside world. var Ui cli.Ui -const ErrorPrefix = "e:" -const OutputPrefix = "o:" +const ( + ErrorPrefix = "e:" + OutputPrefix = "o:" +) func init() { Ui = &cli.PrefixedUi{ diff --git a/state/state.go b/state/state.go index 456059df8..747723e19 100644 --- a/state/state.go +++ b/state/state.go @@ -4,6 +4,14 @@ import ( "github.com/hashicorp/terraform/terraform" ) +// State is the collection of all state interfaces. +type State interface { + StateReader + StateWriter + StateRefresher + StatePersister +} + // StateReader is the interface for things that can return a state. Retrieving // the state here must not error. Loading the state fresh (an operation that // can likely error) should be implemented by RefreshState. If a state hasn't