command: start migrating to new state package

This commit is contained in:
Mitchell Hashimoto 2015-02-21 16:04:32 -08:00
parent 1eec77378b
commit 579f102f37
8 changed files with 166 additions and 273 deletions

View File

@ -10,6 +10,9 @@ import (
// Set to true when we're testing // Set to true when we're testing
var test bool = false 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. // DefaultStateFilename is the default filename used for the state file.
const DefaultStateFilename = "terraform.tfstate" const DefaultStateFilename = "terraform.tfstate"

View File

@ -10,7 +10,7 @@ import (
"path/filepath" "path/filepath"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"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"
"github.com/mitchellh/colorstring" "github.com/mitchellh/colorstring"
@ -24,7 +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 *terraform.State state state.State
// 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
@ -78,11 +78,6 @@ func (m *Meta) initStatePaths() {
// StateOutPath returns the true output path for the state file // StateOutPath returns the true output path for the state file
func (m *Meta) StateOutPath() string { func (m *Meta) StateOutPath() string {
m.initStatePaths()
if m.useRemoteState {
path, _ := remote.HiddenStatePath()
return path
}
return m.stateOutPath return m.stateOutPath
} }
@ -132,11 +127,12 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
} }
// Store the loaded state // Store the loaded state
state, err := m.loadState() state, statePath, err := State(m.statePath)
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
m.state = state m.state = state
m.stateOutPath = statePath
// Load the root module // Load the root module
mod, err := module.NewTreeModule("", copts.Path) mod, err := module.NewTreeModule("", copts.Path)
@ -154,7 +150,7 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
} }
opts.Module = mod opts.Module = mod
opts.State = state opts.State = state.State()
ctx := terraform.NewContext(opts) ctx := terraform.NewContext(opts)
return ctx, false, nil return ctx, false, nil
} }
@ -175,6 +171,21 @@ func (m *Meta) InputMode() terraform.InputMode {
return mode 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. // UIInput returns a UIInput object to be used for asking for input.
func (m *Meta) UIInput() terraform.UIInput { func (m *Meta) UIInput() terraform.UIInput {
return &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 // PersistState is used to write out the state, handling backup of
// the existing state file and respecting path configurations. // the existing state file and respecting path configurations.
func (m *Meta) PersistState(s *terraform.State) error { func (m *Meta) PersistState(s *terraform.State) error {
if m.useRemoteState { if err := m.state.WriteState(s); err != nil {
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 {
return err 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 return m.state.PersistState()
// 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
} }
// Input returns true if we should ask for input for context. // Input returns true if we should ask for input for context.

View File

@ -7,7 +7,6 @@ import (
"reflect" "reflect"
"testing" "testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -182,156 +181,3 @@ func TestMeta_initStatePaths(t *testing.T) {
t.Fatalf("bad: %#v", m) 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")
}
}

View File

@ -32,12 +32,13 @@ func (c *OutputCommand) Run(args []string) int {
} }
name := args[0] name := args[0]
state, err := c.Meta.loadState() stateStore, err := c.Meta.State()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
return 1 return 1
} }
state := stateStore.State()
if len(state.RootModule().Outputs) == 0 { if len(state.RootModule().Outputs) == 0 {
c.Ui.Error(fmt.Sprintf( c.Ui.Error(fmt.Sprintf(
"The state file has no outputs defined. Define an output\n" + "The state file has no outputs defined. Define an output\n" +

View File

@ -6,6 +6,7 @@ import (
"os" "os"
"strings" "strings"
statelib "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -36,7 +37,7 @@ func (c *ShowCommand) Run(args []string) int {
return 1 return 1
} }
var err, planErr, stateErr error var planErr, stateErr error
var path string var path string
var plan *terraform.Plan var plan *terraform.Plan
var state *terraform.State var state *terraform.State
@ -68,12 +69,13 @@ func (c *ShowCommand) Run(args []string) int {
} else { } else {
// We should use the default state if it exists. // We should use the default state if it exists.
c.Meta.statePath = DefaultStateFilename stateStore := &statelib.LocalState{Path: DefaultStateFilename}
state, err = c.Meta.loadState() if err := stateStore.RefreshState(); err != nil {
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err)) c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
return 1 return 1
} }
state = stateStore.State()
if state == nil { if state == nil {
c.Ui.Output("No state.") c.Ui.Output("No state.")
return 0 return 0

121
command/state.go Normal file
View File

@ -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
}

View File

@ -14,8 +14,10 @@ var Commands map[string]cli.CommandFactory
// Ui is the cli.Ui used for communicating to the outside world. // Ui is the cli.Ui used for communicating to the outside world.
var Ui cli.Ui var Ui cli.Ui
const ErrorPrefix = "e:" const (
const OutputPrefix = "o:" ErrorPrefix = "e:"
OutputPrefix = "o:"
)
func init() { func init() {
Ui = &cli.PrefixedUi{ Ui = &cli.PrefixedUi{

View File

@ -4,6 +4,14 @@ import (
"github.com/hashicorp/terraform/terraform" "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 // 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 // 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 // can likely error) should be implemented by RefreshState. If a state hasn't