Merge pull request #1028 from hashicorp/f-continuous-state

core: state management refactor to prepare for continuous state
This commit is contained in:
Mitchell Hashimoto 2015-02-23 15:14:25 -08:00
commit 41750dfa05
52 changed files with 2202 additions and 2579 deletions

View File

@ -15,7 +15,6 @@ import (
"testing" "testing"
"time" "time"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -408,8 +407,9 @@ func TestApply_plan_remoteState(t *testing.T) {
defer func() { test = true }() defer func() { test = true }()
tmp, cwd := testCwd(t) tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd) defer testFixCwd(t, tmp, cwd)
if err := remote.EnsureDirectory(); err != nil { remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
t.Fatalf("err: %v", err) if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil {
t.Fatalf("err: %s", err)
} }
// Set some default reader/writers for the inputs // Set some default reader/writers for the inputs
@ -448,21 +448,13 @@ func TestApply_plan_remoteState(t *testing.T) {
} }
// State file should be not be installed // State file should be not be installed
exists, err := remote.ExistsFile(DefaultStateFilename) if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil {
if err != nil {
t.Fatalf("err: %v", err)
}
if exists {
t.Fatalf("State path should not exist") t.Fatalf("State path should not exist")
} }
// Check for remote state // Check for remote state
output, _, err := remote.ReadLocalState() if _, err := os.Stat(remoteStatePath); err != nil {
if err != nil { t.Fatalf("missing remote state: %s", err)
t.Fatalf("err: %v", err)
}
if output == nil {
t.Fatalf("missing remote state")
} }
} }

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

@ -178,17 +178,20 @@ func testTempDir(t *testing.T) string {
// testCwdDir is used to change the current working directory // testCwdDir is used to change the current working directory
// into a test directory that should be remoted after // into a test directory that should be remoted after
func testCwd(t *testing.T) (string, string) { func testCwd(t *testing.T) (string, string) {
tmp, err := ioutil.TempDir("", "remote") tmp, err := ioutil.TempDir("", "tf")
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
cwd, err := os.Getwd() cwd, err := os.Getwd()
if err != nil { if err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
if err := os.Chdir(tmp); err != nil { if err := os.Chdir(tmp); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
return tmp, cwd return tmp, cwd
} }
@ -197,6 +200,7 @@ func testFixCwd(t *testing.T, tmp, cwd string) {
if err := os.Chdir(cwd); err != nil { if err := os.Chdir(cwd); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }
if err := os.RemoveAll(tmp); err != nil { if err := os.RemoveAll(tmp); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %v", err)
} }

View File

@ -9,15 +9,15 @@ import (
"github.com/mitchellh/go-homedir" "github.com/mitchellh/go-homedir"
) )
// FlagVar is a flag.Value implementation for parsing user variables // FlagKV is a flag.Value implementation for parsing user variables
// from the command-line in the format of '-var key=value'. // from the command-line in the format of '-var key=value'.
type FlagVar map[string]string type FlagKV map[string]string
func (v *FlagVar) String() string { func (v *FlagKV) String() string {
return "" return ""
} }
func (v *FlagVar) Set(raw string) error { func (v *FlagKV) Set(raw string) error {
idx := strings.Index(raw, "=") idx := strings.Index(raw, "=")
if idx == -1 { if idx == -1 {
return fmt.Errorf("No '=' value in arg: %s", raw) return fmt.Errorf("No '=' value in arg: %s", raw)
@ -32,16 +32,16 @@ func (v *FlagVar) Set(raw string) error {
return nil return nil
} }
// FlagVarFile is a flag.Value implementation for parsing user variables // FlagKVFile is a flag.Value implementation for parsing user variables
// from the command line in the form of files. i.e. '-var-file=foo' // from the command line in the form of files. i.e. '-var-file=foo'
type FlagVarFile map[string]string type FlagKVFile map[string]string
func (v *FlagVarFile) String() string { func (v *FlagKVFile) String() string {
return "" return ""
} }
func (v *FlagVarFile) Set(raw string) error { func (v *FlagKVFile) Set(raw string) error {
vs, err := loadVarFile(raw) vs, err := loadKVFile(raw)
if err != nil { if err != nil {
return err return err
} }
@ -57,7 +57,7 @@ func (v *FlagVarFile) Set(raw string) error {
return nil return nil
} }
func loadVarFile(rawPath string) (map[string]string, error) { func loadKVFile(rawPath string) (map[string]string, error) {
path, err := homedir.Expand(rawPath) path, err := homedir.Expand(rawPath)
if err != nil { if err != nil {
return nil, fmt.Errorf( return nil, fmt.Errorf(

View File

@ -7,11 +7,11 @@ import (
"testing" "testing"
) )
func TestFlagVar_impl(t *testing.T) { func TestFlagKV_impl(t *testing.T) {
var _ flag.Value = new(FlagVar) var _ flag.Value = new(FlagKV)
} }
func TestFlagVar(t *testing.T) { func TestFlagKV(t *testing.T) {
cases := []struct { cases := []struct {
Input string Input string
Output map[string]string Output map[string]string
@ -43,7 +43,7 @@ func TestFlagVar(t *testing.T) {
} }
for _, tc := range cases { for _, tc := range cases {
f := new(FlagVar) f := new(FlagKV)
err := f.Set(tc.Input) err := f.Set(tc.Input)
if (err != nil) != tc.Error { if (err != nil) != tc.Error {
t.Fatalf("bad error. Input: %#v", tc.Input) t.Fatalf("bad error. Input: %#v", tc.Input)
@ -56,11 +56,11 @@ func TestFlagVar(t *testing.T) {
} }
} }
func TestFlagVarFile_impl(t *testing.T) { func TestFlagKVFile_impl(t *testing.T) {
var _ flag.Value = new(FlagVarFile) var _ flag.Value = new(FlagKVFile)
} }
func TestFlagVarFile(t *testing.T) { func TestFlagKVFile(t *testing.T) {
inputLibucl := ` inputLibucl := `
foo = "bar" foo = "bar"
` `
@ -93,7 +93,7 @@ foo = "bar"
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
f := new(FlagVarFile) f := new(FlagKVFile)
err := f.Set(path) err := f.Set(path)
if (err != nil) != tc.Error { if (err != nil) != tc.Error {
t.Fatalf("bad error. Input: %#v", tc.Input) t.Fatalf("bad error. Input: %#v", tc.Input)

View File

@ -8,7 +8,6 @@ import (
"github.com/hashicorp/terraform/config" "github.com/hashicorp/terraform/config"
"github.com/hashicorp/terraform/config/module" "github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -19,14 +18,12 @@ type InitCommand struct {
} }
func (c *InitCommand) Run(args []string) int { func (c *InitCommand) Run(args []string) int {
var remoteBackend, remoteAddress, remoteAccessToken, remoteName, remotePath string var remoteBackend string
args = c.Meta.process(args, false) args = c.Meta.process(args, false)
remoteConfig := make(map[string]string)
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError) cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
cmdFlags.StringVar(&remoteBackend, "backend", "atlas", "") cmdFlags.StringVar(&remoteBackend, "backend", "", "")
cmdFlags.StringVar(&remoteAddress, "address", "", "") cmdFlags.Var((*FlagKV)(&remoteConfig), "backend-config", "config")
cmdFlags.StringVar(&remoteAccessToken, "access-token", "", "")
cmdFlags.StringVar(&remoteName, "name", "", "")
cmdFlags.StringVar(&remotePath, "path", "", "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil { if err := cmdFlags.Parse(args); err != nil {
return 1 return 1
@ -92,37 +89,34 @@ func (c *InitCommand) Run(args []string) int {
} }
// Handle remote state if configured // Handle remote state if configured
if remoteAddress != "" || remoteAccessToken != "" || remoteName != "" || remotePath != "" { if remoteBackend != "" {
var remoteConf terraform.RemoteState var remoteConf terraform.RemoteState
remoteConf.Type = remoteBackend remoteConf.Type = remoteBackend
remoteConf.Config = map[string]string{ remoteConf.Config = remoteConfig
"address": remoteAddress,
"access_token": remoteAccessToken,
"name": remoteName,
"path": remotePath,
}
// Ensure remote state is not already enabled state, err := c.State()
haveLocal, err := remote.HaveLocalState()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err)) c.Ui.Error(fmt.Sprintf("Error checking for state: %s", err))
return 1 return 1
} }
if haveLocal { if state != nil {
c.Ui.Error("Remote state is already enabled. Aborting.") s := state.State()
if !s.Empty() {
c.Ui.Error(fmt.Sprintf(
"State file already exists and is not empty! Please remove this\n" +
"state file before initializing. Note that removing the state file\n" +
"may result in a loss of information since Terraform uses this\n" +
"to track your infrastructure."))
return 1 return 1
} }
if s.IsRemote() {
// Check if we have the non-managed state file c.Ui.Error(fmt.Sprintf(
haveNonManaged, err := remote.ExistsFile(DefaultStateFilename) "State file already exists with remote state enabled! Please remove this\n" +
if err != nil { "state file before initializing. Note that removing the state file\n" +
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err)) "may result in a loss of information since Terraform uses this\n" +
"to track your infrastructure."))
return 1 return 1
} }
if haveNonManaged {
c.Ui.Error(fmt.Sprintf("Existing state file '%s' found. Aborting.",
DefaultStateFilename))
return 1
} }
// Initialize a blank state file with remote enabled // Initialize a blank state file with remote enabled
@ -149,20 +143,11 @@ Usage: terraform init [options] SOURCE [PATH]
Options: Options:
-address=url URL of the remote storage server. -backend=atlas Specifies the type of remote backend. If not
Required for HTTP backend, optional for Atlas and Consul. specified, local storage will be used.
-access-token=token Authentication token for state storage server. -backend-config="k=v" Specifies configuration for the remote storage
Required for Atlas backend, optional for Consul. backend. This can be specified multiple times.
-backend=atlas Specifies the type of remote backend. Must be one
of Atlas, Consul, or HTTP. Defaults to atlas.
-name=name Name of the state file in the state storage server.
Required for Atlas backend.
-path=path Path of the remote state in Consul. Required for the
Consul backend.
` `
return strings.TrimSpace(helpText) return strings.TrimSpace(helpText)

View File

@ -5,7 +5,6 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -163,7 +162,7 @@ func TestInit_remoteState(t *testing.T) {
args := []string{ args := []string{
"-backend", "http", "-backend", "http",
"-address", conf.Config["address"], "-backend-config", "address=" + conf.Config["address"],
testFixturePath("init"), testFixturePath("init"),
tmp, tmp,
} }
@ -175,9 +174,80 @@ func TestInit_remoteState(t *testing.T) {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
path, _ := remote.HiddenStatePath() if _, err := os.Stat(filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)); err != nil {
_, err := os.Stat(path) t.Fatalf("missing state: %s", err)
}
}
func TestInit_remoteStateWithLocal(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
statePath := filepath.Join(tmp, DefaultStateFilename)
// Write some state
f, err := os.Create(statePath)
if err != nil { if err != nil {
t.Fatalf("missing state") t.Fatalf("err: %s", err)
}
err = terraform.WriteState(testState(), f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-backend", "http",
"-backend-config", "address=http://google.com",
testFixturePath("init"),
}
if code := c.Run(args); code == 0 {
t.Fatalf("should have failed: \n%s", ui.OutputWriter.String())
}
}
func TestInit_remoteStateWithRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
t.Fatalf("err: %s", err)
}
// Write some state
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(testState(), f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
c := &InitCommand{
Meta: Meta{
ContextOpts: testCtxConfig(testProvider()),
Ui: ui,
},
}
args := []string{
"-backend", "http",
"-backend-config", "address=http://google.com",
testFixturePath("init"),
}
if code := c.Run(args); code == 0 {
t.Fatalf("should have failed: \n%s", ui.OutputWriter.String())
} }
} }

View File

@ -5,12 +5,11 @@ import (
"flag" "flag"
"fmt" "fmt"
"io" "io"
"log"
"os" "os"
"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 +23,8 @@ 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
stateResult *StateResult
// 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
@ -41,24 +41,22 @@ type Meta struct {
color bool color bool
oldUi cli.Ui oldUi cli.Ui
// useRemoteState is enabled if we are using remote state storage // The fields below are expected to be set by the command via
// This is set when the context is loaded if we read from a remote // command line flags. See the Apply command for an example.
// 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
// is simply no state. // is simply no state.
statePath string //
// stateOutPath is used to override the output path for the state. // stateOutPath is used to override the output path for the state.
// If not provided, the StatePath is used causing the old state to // If not provided, the StatePath is used causing the old state to
// be overriden. // be overriden.
stateOutPath string //
// backupPath is used to backup the state file before writing a modified // backupPath is used to backup the state file before writing a modified
// version. It defaults to stateOutPath + DefaultBackupExtention // version. It defaults to stateOutPath + DefaultBackupExtention
statePath string
stateOutPath string
backupPath string backupPath string
} }
@ -78,11 +76,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
} }
@ -106,14 +99,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" +
@ -132,11 +127,10 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
} }
// Store the loaded state // Store the loaded state
state, err := m.loadState() state, err := m.State()
if err != nil { if err != nil {
return nil, false, err return nil, false, err
} }
m.state = state
// Load the root module // Load the root module
mod, err := module.NewTreeModule("", copts.Path) mod, err := module.NewTreeModule("", copts.Path)
@ -154,7 +148,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 +169,53 @@ 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
}
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)
return &StateOpts{
LocalPath: localPath,
LocalPathOut: m.stateOutPath,
RemotePath: remotePath,
RemoteRefresh: true,
BackupPath: m.backupPath,
}
}
// 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 +223,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.
@ -329,11 +269,11 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
func (m *Meta) flagSet(n string) *flag.FlagSet { func (m *Meta) flagSet(n string) *flag.FlagSet {
f := flag.NewFlagSet(n, flag.ContinueOnError) f := flag.NewFlagSet(n, flag.ContinueOnError)
f.BoolVar(&m.input, "input", true, "input") f.BoolVar(&m.input, "input", true, "input")
f.Var((*FlagVar)(&m.variables), "var", "variables") f.Var((*FlagKV)(&m.variables), "var", "variables")
f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file") f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
if m.autoKey != "" { if m.autoKey != "" {
f.Var((*FlagVarFile)(&m.autoVariables), m.autoKey, "variable file") f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file")
} }
// Create an io.Writer that writes to our Ui properly for errors. // Create an io.Writer that writes to our Ui properly for errors.

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

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/state"
) )
type PullCommand struct { type PullCommand struct {
@ -20,32 +20,50 @@ func (c *PullCommand) Run(args []string) int {
return 1 return 1
} }
// Recover the local state if any // Read out our state
local, _, err := remote.ReadLocalState() s, err := c.State()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err)) c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1 return 1
} }
if local == nil || local.Remote == nil { localState := s.State()
// If remote state isn't enabled, it is a problem.
if !localState.IsRemote() {
c.Ui.Error("Remote state not enabled!") c.Ui.Error("Remote state not enabled!")
return 1 return 1
} }
// Attempt the state refresh // We need the CacheState structure in order to do anything
change, err := remote.RefreshState(local.Remote) var cache *state.CacheState
if err != nil { if bs, ok := s.(*state.BackupState); ok {
if cs, ok := bs.Real.(*state.CacheState); ok {
cache = cs
}
}
if cache == nil {
c.Ui.Error(fmt.Sprintf( c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %v", err)) "Failed to extract internal CacheState from remote state.\n" +
"This is an internal error, please report it as a bug."))
return 1
}
// Refresh the state
if err := cache.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
return 1 return 1
} }
// Use an error exit code if the update was not a success // Use an error exit code if the update was not a success
change := cache.RefreshResult()
if !change.SuccessfulPull() { if !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change)) c.Ui.Error(fmt.Sprintf("%s", change))
return 1 return 1
} else { } else {
c.Ui.Output(fmt.Sprintf("%s", change)) c.Ui.Output(fmt.Sprintf("%s", change))
} }
return 0 return 0
} }

View File

@ -7,9 +7,10 @@ import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"os"
"path/filepath"
"testing" "testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -46,10 +47,19 @@ func TestPull_local(t *testing.T) {
defer srv.Close() defer srv.Close()
// Store the local state // Store the local state
buf := bytes.NewBuffer(nil) statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
terraform.WriteState(s, buf) if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
remote.EnsureDirectory() t.Fatalf("err: %s", err)
remote.Persist(buf) }
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(s, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PullCommand{ c := &PullCommand{

View File

@ -5,7 +5,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/state"
) )
type PushCommand struct { type PushCommand struct {
@ -22,31 +22,52 @@ func (c *PushCommand) Run(args []string) int {
return 1 return 1
} }
// Check for a remote state file // Read out our state
local, _, err := remote.ReadLocalState() s, err := c.State()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err)) c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
return 1 return 1
} }
if local == nil || local.Remote == nil { localState := s.State()
// If remote state isn't enabled, it is a problem.
if !localState.IsRemote() {
c.Ui.Error("Remote state not enabled!") c.Ui.Error("Remote state not enabled!")
return 1 return 1
} }
// Attempt to push the state // We need the CacheState structure in order to do anything
change, err := remote.PushState(local.Remote, force) var cache *state.CacheState
if err != nil { if bs, ok := s.(*state.BackupState); ok {
c.Ui.Error(fmt.Sprintf("Failed to push state: %v", err)) if cs, ok := bs.Real.(*state.CacheState); ok {
cache = cs
}
}
if cache == nil {
c.Ui.Error(fmt.Sprintf(
"Failed to extract internal CacheState from remote state.\n" +
"This is an internal error, please report it as a bug."))
return 1 return 1
} }
// Use an error exit code if the update was not a success // Refresh the cache state
if !change.SuccessfulPush() { if err := cache.Cache.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("%s", change)) c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
return 1 return 1
} else {
c.Ui.Output(fmt.Sprintf("%s", change))
} }
// Write it to the real storage
remote := cache.Durable
if err := remote.WriteState(cache.Cache.State()); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state: %s", err))
return 1
}
if err := remote.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error saving state: %s", err))
return 1
}
return 0 return 0
} }

View File

@ -1,10 +1,10 @@
package command package command
import ( import (
"bytes" "os"
"path/filepath"
"testing" "testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli" "github.com/mitchellh/cli"
) )
@ -41,10 +41,19 @@ func TestPush_local(t *testing.T) {
s.Remote = conf s.Remote = conf
// Store the local state // Store the local state
buf := bytes.NewBuffer(nil) statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
terraform.WriteState(s, buf) if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
remote.EnsureDirectory() t.Fatalf("err: %s", err)
remote.Persist(buf) }
f, err := os.Create(statePath)
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(s, f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi) ui := new(cli.MockUi)
c := &PushCommand{ c := &PushCommand{

View File

@ -5,8 +5,6 @@ import (
"log" "log"
"os" "os"
"strings" "strings"
"github.com/hashicorp/terraform/remote"
) )
// RefreshCommand is a cli.Command implementation that refreshes the state // RefreshCommand is a cli.Command implementation that refreshes the state
@ -44,16 +42,16 @@ func (c *RefreshCommand) Run(args []string) int {
} }
// Check if remote state is enabled // Check if remote state is enabled
remoteEnabled, err := remote.HaveLocalState() state, err := c.State()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to check for remote state: %v", err)) c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
return 1 return 1
} }
// Verify that the state path exists. The "ContextArg" function below // Verify that the state path exists. The "ContextArg" function below
// will actually do this, but we want to provide a richer error message // will actually do this, but we want to provide a richer error message
// if possible. // if possible.
if !remoteEnabled { if !state.State().IsRemote() {
if _, err := os.Stat(c.Meta.statePath); err != nil { if _, err := os.Stat(c.Meta.statePath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
c.Ui.Error(fmt.Sprintf( c.Ui.Error(fmt.Sprintf(
@ -95,14 +93,14 @@ func (c *RefreshCommand) Run(args []string) int {
return 1 return 1
} }
state, err := ctx.Refresh() newState, err := ctx.Refresh()
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err)) c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
return 1 return 1
} }
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath()) log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
if err := c.Meta.PersistState(state); err != nil { if err := c.Meta.PersistState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err)) c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
return 1 return 1
} }

View File

@ -1,15 +1,14 @@
package command package command
import ( import (
"bytes"
"flag" "flag"
"fmt" "fmt"
"io/ioutil"
"log" "log"
"os" "os"
"strings" "strings"
"github.com/hashicorp/terraform/remote" "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform" "github.com/hashicorp/terraform/terraform"
) )
@ -32,81 +31,81 @@ type RemoteCommand struct {
func (c *RemoteCommand) Run(args []string) int { func (c *RemoteCommand) Run(args []string) int {
args = c.Meta.process(args, false) args = c.Meta.process(args, false)
var address, accessToken, name, path string config := make(map[string]string)
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError) cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "") cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "") cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path") cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path") cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "") cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
cmdFlags.StringVar(&address, "address", "", "") cmdFlags.Var((*FlagKV)(&config), "backend-config", "config")
cmdFlags.StringVar(&accessToken, "access-token", "", "")
cmdFlags.StringVar(&name, "name", "", "")
cmdFlags.StringVar(&path, "path", "", "")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) } cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil { if err := cmdFlags.Parse(args); err != nil {
return 1 return 1
} }
// Show help if given no inputs // Show help if given no inputs
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" && if !c.conf.disableRemote && c.remoteConf.Type == "atlas" && len(config) == 0 {
name == "" && accessToken == "" {
cmdFlags.Usage() cmdFlags.Usage()
return 1 return 1
} }
// Set the local state path
c.statePath = c.conf.statePath
// Populate the various configurations // Populate the various configurations
c.remoteConf.Config = map[string]string{ c.remoteConf.Config = config
"address": address,
"access_token": accessToken,
"name": name,
"path": path,
}
// Check if have an existing local state file // Get the state information. We specifically request the cache only
haveLocal, err := remote.HaveLocalState() // for the remote state here because it is possible the remote state
if err != nil { // is invalid and we don't want to error.
c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err)) 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 return 1
} }
// Check if we have the non-managed state file // Get the local and remote [cached] state
haveNonManaged, err := remote.ExistsFile(c.conf.statePath) localState := c.stateResult.Local.State()
if err != nil { var remoteState *terraform.State
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err)) if remote := c.stateResult.Remote; remote != nil {
return 1 remoteState = remote.State()
} }
// Check if remote state is being disabled // Check if remote state is being disabled
if c.conf.disableRemote { if c.conf.disableRemote {
if !haveLocal { if !remoteState.IsRemote() {
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting.")) c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
return 1 return 1
} }
if haveNonManaged { if !localState.Empty() {
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.", c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
c.conf.statePath)) c.conf.statePath))
return 1 return 1
} }
return c.disableRemoteState() return c.disableRemoteState()
} }
// Ensure there is no conflict // Ensure there is no conflict
haveCache := !remoteState.Empty()
haveLocal := !localState.Empty()
switch { 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.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
c.conf.statePath)) c.conf.statePath))
return 1 return 1
case !haveLocal && !haveNonManaged: case !haveCache && !haveLocal:
// If we don't have either state file, initialize a blank state file // If we don't have either state file, initialize a blank state file
return c.initBlankState() return c.initBlankState()
case haveLocal && !haveNonManaged: case haveCache && !haveLocal:
// Update the remote state target potentially // Update the remote state target potentially
return c.updateRemoteConfig() return c.updateRemoteConfig()
case !haveLocal && haveNonManaged: case !haveCache && haveLocal:
// Enable remote state management // Enable remote state management
return c.enableRemoteState() return c.enableRemoteState()
} }
@ -117,71 +116,66 @@ func (c *RemoteCommand) Run(args []string) int {
// disableRemoteState is used to disable remote state management, // disableRemoteState is used to disable remote state management,
// and move the state file into place. // and move the state file into place.
func (c *RemoteCommand) disableRemoteState() int { func (c *RemoteCommand) disableRemoteState() int {
// Get the local state if c.stateResult == nil {
local, _, err := remote.ReadLocalState() c.Ui.Error(fmt.Sprintf(
if err != nil { "Internal error. State() must be called internally before remote\n" +
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err)) "state can be disabled. Please report this as a bug."))
return 1 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 // Ensure we have the latest state before disabling
if c.conf.pullOnDisable { if c.conf.pullOnDisable {
log.Printf("[INFO] Refreshing local state from remote server") log.Printf("[INFO] Refreshing local state from remote server")
change, err := remote.RefreshState(local.Remote) if err := remote.RefreshState(); err != nil {
if err != nil {
c.Ui.Error(fmt.Sprintf( c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %v", err)) "Failed to refresh from remote state: %s", err))
return 1 return 1
} }
// Exit if we were unable to update // Exit if we were unable to update
if !change.SuccessfulPull() { if change := remote.RefreshResult(); !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change)) c.Ui.Error(fmt.Sprintf("%s", change))
return 1 return 1
} else { } else {
log.Printf("[INFO] %s", change) 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 // Clear the remote management, and copy into place
local.Remote = nil newState := remote.State()
fh, err := os.Create(c.conf.statePath) newState.Remote = nil
if err != nil { if err := local.WriteState(newState); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to create state file '%s': %v", c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.conf.statePath, err)) c.conf.statePath, err))
return 1 return 1
} }
defer fh.Close() if err := local.PersistState(); err != nil {
if err := terraform.WriteState(local, fh); err != nil { c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %v",
c.conf.statePath, err)) c.conf.statePath, err))
return 1 return 1
} }
// Remove the old state file // Remove the old state file
path, err := remote.HiddenStatePath() if err := os.Remove(c.stateResult.RemotePath); err != nil {
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 {
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err)) c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
return 1 return 1
} }
return 0 return 0
} }
// validateRemoteConfig is used to verify that the remote configuration // validateRemoteConfig is used to verify that the remote configuration
// we have is valid // we have is valid
func (c *RemoteCommand) validateRemoteConfig() error { func (c *RemoteCommand) validateRemoteConfig() error {
err := remote.ValidConfig(&c.remoteConf) conf := c.remoteConf
_, err := remote.NewClient(conf.Type, conf.Config)
if err != nil { if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err)) c.Ui.Error(fmt.Sprintf("%s", err))
} }
@ -196,18 +190,17 @@ func (c *RemoteCommand) initBlankState() int {
return 1 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 // Make a blank state, attach the remote configuration
blank := terraform.NewState() blank := terraform.NewState()
blank.Remote = &c.remoteConf blank.Remote = &c.remoteConf
// Persist the state // 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)) c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
return 1 return 1
} }
@ -225,16 +218,17 @@ func (c *RemoteCommand) updateRemoteConfig() int {
return 1 return 1
} }
// Read in the local state // Read in the local state, which is just the cache of the remote state
local, _, err := remote.ReadLocalState() remote := c.stateResult.Remote.Cache
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
return 1
}
// Update the configuration // Update the configuration
local.Remote = &c.remoteConf state := remote.State()
if err := remote.PersistState(local); err != nil { 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)) c.Ui.Error(fmt.Sprintf("%s", err))
return 1 return 1
} }
@ -252,21 +246,10 @@ func (c *RemoteCommand) enableRemoteState() int {
return 1 return 1
} }
// Make the hidden directory // Read the local state
if err := remote.EnsureDirectory(); err != nil { local := c.stateResult.Local
c.Ui.Error(fmt.Sprintf("%s", err)) if err := local.RefreshState(); err != nil {
return 1 c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
}
// 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))
return 1 return 1
} }
@ -279,25 +262,31 @@ func (c *RemoteCommand) enableRemoteState() int {
} }
log.Printf("[INFO] Writing backup state to: %s", backupPath) log.Printf("[INFO] Writing backup state to: %s", backupPath)
f, err := os.Create(backupPath) backup := &state.LocalState{Path: backupPath}
if err == nil { if err := backup.WriteState(local.State()); err != nil {
err = terraform.WriteState(state, f) c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
f.Close() return 1
} }
if err != nil { if err := backup.PersistState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err)) c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
return 1 return 1
} }
} }
// Update the local configuration, move into place // Update the local configuration, move into place
state := local.State()
state.Remote = &c.remoteConf 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)) c.Ui.Error(fmt.Sprintf("%s", err))
return 1 return 1
} }
// Remove the state file // Remove the original, local state file
log.Printf("[INFO] Removing state file: %s", c.conf.statePath) log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
if err := os.Remove(c.conf.statePath); err != nil { if err := os.Remove(c.conf.statePath); err != nil {
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v", c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
@ -321,15 +310,12 @@ Usage: terraform remote [options]
Options: Options:
-address=url URL of the remote storage server.
Required for HTTP backend, optional for Atlas and Consul.
-access-token=token Authentication token for state storage server.
Required for Atlas backend, optional for Consul.
-backend=Atlas Specifies the type of remote backend. Must be one -backend=Atlas Specifies the type of remote backend. Must be one
of Atlas, Consul, or HTTP. Defaults to Atlas. of Atlas, Consul, or HTTP. Defaults to Atlas.
-backend-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
-backup=path Path to backup the existing state file before -backup=path Path to backup the existing state file before
modifying. Defaults to the "-state" path with modifying. Defaults to the "-state" path with
".backup" extension. Set to "-" to disable backup. ".backup" extension. Set to "-" to disable backup.
@ -337,12 +323,6 @@ Options:
-disable Disables remote state management and migrates the state -disable Disables remote state management and migrates the state
to the -state path. to the -state path.
-name=name Name of the state file in the state storage server.
Required for Atlas backend.
-path=path Path of the remote state in Consul. Required for the
Consul backend.
-pull=true Controls if the remote state is pulled before disabling. -pull=true Controls if the remote state is pulled before disabling.
This defaults to true to ensure the latest state is cached This defaults to true to ensure the latest state is cached
before disabling. before disabling.

View File

@ -4,9 +4,10 @@ import (
"bytes" "bytes"
"io/ioutil" "io/ioutil"
"os" "os"
"path/filepath"
"testing" "testing"
"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"
) )
@ -26,11 +27,15 @@ func TestRemote_disable(t *testing.T) {
s = terraform.NewState() s = terraform.NewState()
s.Serial = 5 s.Serial = 5
s.Remote = conf 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 { if err := state.PersistState(); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %s", err)
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
@ -45,23 +50,9 @@ func TestRemote_disable(t *testing.T) {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
// Local state file should be removed // Local state file should be removed and the local cache should exist
haveLocal, err := remote.HaveLocalState() testRemoteLocal(t, true)
if err != nil { testRemoteLocalCache(t, false)
t.Fatalf("err: %v", err)
}
if haveLocal {
t.Fatalf("should be disabled")
}
// New state file should be installed
exists, err := remote.ExistsFile(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %v", err)
}
if !exists {
t.Fatalf("failed to make state file")
}
// Check that the state file was updated // Check that the state file was updated
raw, _ := ioutil.ReadFile(DefaultStateFilename) raw, _ := ioutil.ReadFile(DefaultStateFilename)
@ -71,11 +62,6 @@ func TestRemote_disable(t *testing.T) {
} }
// Ensure we updated // 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 { if newState.Remote != nil {
t.Fatalf("remote configuration not removed") t.Fatalf("remote configuration not removed")
} }
@ -96,11 +82,15 @@ func TestRemote_disable_noPull(t *testing.T) {
s = terraform.NewState() s = terraform.NewState()
s.Serial = 5 s.Serial = 5
s.Remote = conf 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 { if err := state.PersistState(); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %s", err)
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
@ -115,23 +105,9 @@ func TestRemote_disable_noPull(t *testing.T) {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
// Local state file should be removed // Local state file should be removed and the local cache should exist
haveLocal, err := remote.HaveLocalState() testRemoteLocal(t, true)
if err != nil { testRemoteLocalCache(t, false)
t.Fatalf("err: %v", err)
}
if haveLocal {
t.Fatalf("should be disabled")
}
// New state file should be installed
exists, err := remote.ExistsFile(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %v", err)
}
if !exists {
t.Fatalf("failed to make state file")
}
// Check that the state file was updated // Check that the state file was updated
raw, _ := ioutil.ReadFile(DefaultStateFilename) raw, _ := ioutil.ReadFile(DefaultStateFilename)
@ -140,12 +116,6 @@ func TestRemote_disable_noPull(t *testing.T) {
t.Fatalf("err: %v", err) 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 { if newState.Remote != nil {
t.Fatalf("remote configuration not removed") t.Fatalf("remote configuration not removed")
} }
@ -178,11 +148,15 @@ func TestRemote_disable_otherState(t *testing.T) {
// Persist local remote state // Persist local remote state
s := terraform.NewState() s := terraform.NewState()
s.Serial = 5 s.Serial = 5
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 { if err := state.PersistState(); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %s", err)
} }
// Also put a file at the default path // Also put a file at the default path
@ -218,11 +192,15 @@ func TestRemote_managedAndNonManaged(t *testing.T) {
// Persist local remote state // Persist local remote state
s := terraform.NewState() s := terraform.NewState()
s.Serial = 5 s.Serial = 5
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 { if err := state.PersistState(); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %s", err)
} }
// Also put a file at the default path // Also put a file at the default path
@ -265,18 +243,20 @@ func TestRemote_initBlank(t *testing.T) {
args := []string{ args := []string{
"-backend=http", "-backend=http",
"-address", "http://example.com", "-backend-config", "address=http://example.com",
"-access-token=test", "-backend-config", "access_token=test",
} }
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
local, _, err := remote.ReadLocalState() remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
if err != nil { ls := &state.LocalState{Path: remotePath}
t.Fatalf("err: %v", err) if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
} }
local := ls.State()
if local.Remote.Type != "http" { if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote) t.Fatalf("Bad: %#v", local.Remote)
} }
@ -318,11 +298,15 @@ func TestRemote_updateRemote(t *testing.T) {
s.Remote = &terraform.RemoteState{ s.Remote = &terraform.RemoteState{
Type: "invalid", Type: "invalid",
} }
if err := remote.EnsureDirectory(); err != nil {
t.Fatalf("err: %v", err) // Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
ls := &state.LocalState{Path: statePath}
if err := ls.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
} }
if err := remote.PersistState(s); err != nil { if err := ls.PersistState(); err != nil {
t.Fatalf("err: %v", err) t.Fatalf("err: %s", err)
} }
ui := new(cli.MockUi) ui := new(cli.MockUi)
@ -335,18 +319,19 @@ func TestRemote_updateRemote(t *testing.T) {
args := []string{ args := []string{
"-backend=http", "-backend=http",
"-address", "-backend-config", "address=http://example.com",
"http://example.com", "-backend-config", "access_token=test",
"-access-token=test",
} }
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
local, _, err := remote.ReadLocalState() remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
if err != nil { ls = &state.LocalState{Path: remotePath}
t.Fatalf("err: %v", err) if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
} }
local := ls.State()
if local.Remote.Type != "http" { if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote) t.Fatalf("Bad: %#v", local.Remote)
@ -389,18 +374,19 @@ func TestRemote_enableRemote(t *testing.T) {
args := []string{ args := []string{
"-backend=http", "-backend=http",
"-address", "-backend-config", "address=http://example.com",
"http://example.com", "-backend-config", "access_token=test",
"-access-token=test",
} }
if code := c.Run(args); code != 0 { if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String()) t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
} }
local, _, err := remote.ReadLocalState() remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
if err != nil { ls := &state.LocalState{Path: remotePath}
t.Fatalf("err: %v", err) if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
} }
local := ls.State()
if local.Remote.Type != "http" { if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote) t.Fatalf("Bad: %#v", local.Remote)
@ -412,21 +398,49 @@ func TestRemote_enableRemote(t *testing.T) {
t.Fatalf("Bad: %#v", local.Remote) t.Fatalf("Bad: %#v", local.Remote)
} }
// Backup file should exist // Backup file should exist, state file should not
exist, err := remote.ExistsFile(DefaultStateFilename + DefaultBackupExtention) testRemoteLocal(t, false)
if err != nil { testRemoteLocalBackup(t, true)
t.Fatalf("err: %v", err)
}
if !exist {
t.Fatalf("backup should exist")
} }
// State file should not func testRemoteLocal(t *testing.T, exists bool) {
exist, err = remote.ExistsFile(DefaultStateFilename) _, err := os.Stat(DefaultStateFilename)
if err != nil { if os.IsNotExist(err) && !exists {
t.Fatalf("err: %v", err) return
} }
if exist { if err == nil && exists {
t.Fatalf("state file should not exist") return
} }
t.Fatalf("bad: %#v", err)
}
func testRemoteLocalBackup(t *testing.T, exists bool) {
_, err := os.Stat(DefaultStateFilename + DefaultBackupExtention)
if os.IsNotExist(err) && !exists {
return
}
if err == nil && exists {
return
}
if err == nil && !exists {
t.Fatal("expected local backup to exist")
}
t.Fatalf("bad: %#v", err)
}
func testRemoteLocalCache(t *testing.T, exists bool) {
_, err := os.Stat(filepath.Join(DefaultDataDir, DefaultStateFilename))
if os.IsNotExist(err) && !exists {
return
}
if err == nil && exists {
return
}
if err == nil && !exists {
t.Fatal("expected local cache to exist")
}
t.Fatalf("bad: %#v", err)
} }

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

263
command/state.go Normal file
View File

@ -0,0 +1,263 @@
package command
import (
"fmt"
"os"
"path/filepath"
"github.com/hashicorp/errwrap"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/state/remote"
"github.com/hashicorp/terraform/terraform"
)
// StateOpts are options to get the state for a command.
type StateOpts struct {
// LocalPath is the path where the state is stored locally.
//
// LocalPathOut is the path where the local state will be saved. If this
// isn't set, it will be saved back to LocalPath.
LocalPath string
LocalPathOut string
// RemotePath is the path where the remote state cache would be.
//
// 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
// plus the DefaultBackupExtension.
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) (*StateResult, error) {
result := new(StateResult)
// Get the remote state cache path
if opts.RemotePath != "" {
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
}
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
}
}
// Do we have a local state?
if opts.LocalPath != "" {
local := &state.LocalState{
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 {
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(
"Remote state found, but state file '%s' also present.",
opts.LocalPath)
}
// Empty state
local = nil
}
}
if err != nil {
return nil, errwrap.Wrapf(
"Error reading local state: {{err}}", err)
}
if local != nil {
result.State = local
result.StatePath = opts.LocalPath
if opts.LocalPathOut != "" {
result.StatePath = opts.LocalPathOut
}
}
}
// If we have a result, make sure to back it up
if result.State != nil {
backupPath := result.StatePath + DefaultBackupExtention
if opts.BackupPath != "" {
backupPath = opts.BackupPath
}
result.State = &state.BackupState{
Real: result.State,
Path: backupPath,
}
}
// Return whatever state we have
return result, nil
}
// 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.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")
}
// Initialize the remote client based on the local state
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}}",
local.Remote.Type), err)
}
// Create the remote client
durable := &remote.State{Client: client}
// Create the cached client
cache := &state.CacheState{
Cache: &state.LocalState{Path: localPath},
Durable: durable,
}
if refresh {
// 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(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)
}
}
return cache, nil
}
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 {
return nil, err
}
localState := local.State()
return remoteState(localState, path, refresh)
}

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

@ -1,128 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"os"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestAtlasRemote_Interface(t *testing.T) {
var client interface{} = &AtlasRemoteClient{}
if _, ok := client.(RemoteClient); !ok {
t.Fatalf("does not implement interface")
}
}
func checkAtlas(t *testing.T) {
if os.Getenv("ATLAS_TOKEN") == "" {
t.SkipNow()
}
}
func TestAtlasRemote_Validate(t *testing.T) {
conf := map[string]string{}
if _, err := NewAtlasRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["access_token"] = "test"
conf["name"] = "hashicorp/test-state"
if _, err := NewAtlasRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestAtlasRemote_Validate_envVar(t *testing.T) {
conf := map[string]string{}
if _, err := NewAtlasRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
defer os.Setenv("ATLAS_TOKEN", os.Getenv("ATLAS_TOKEN"))
os.Setenv("ATLAS_TOKEN", "foo")
conf["name"] = "hashicorp/test-state"
if _, err := NewAtlasRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestAtlasRemote(t *testing.T) {
checkAtlas(t)
remote := &terraform.RemoteState{
Type: "atlas",
Config: map[string]string{
"access_token": os.Getenv("ATLAS_TOKEN"),
"name": "hashicorp/test-remote-state",
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
// Get a valid input
inp, err := blankState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
// Delete the state, should be none
err = r.DeleteState()
if err != nil {
t.Fatalf("err: %v", err)
}
// Ensure no state
payload, err := r.GetState()
if err != nil {
t.Fatalf("Err: %v", err)
}
if payload != nil {
t.Fatalf("unexpected payload")
}
// Put the state
err = r.PutState(inp, false)
if err != nil {
t.Fatalf("err: %v", err)
}
// Get it back
payload, err = r.GetState()
if err != nil {
t.Fatalf("Err: %v", err)
}
if payload == nil {
t.Fatalf("unexpected payload")
}
// Check the payload
if !bytes.Equal(payload.MD5, hash) {
t.Fatalf("bad hash: %x %x", payload.MD5, hash)
}
if !bytes.Equal(payload.State, inp) {
t.Errorf("inp: %s", inp)
t.Fatalf("bad response: %s", payload.State)
}
// Delete the state
err = r.DeleteState()
if err != nil {
t.Fatalf("err: %v", err)
}
// Should be gone
payload, err = r.GetState()
if err != nil {
t.Fatalf("Err: %v", err)
}
if payload != nil {
t.Fatalf("unexpected payload")
}
}

View File

@ -1,65 +0,0 @@
package remote
import (
"fmt"
"strings"
"github.com/hashicorp/terraform/terraform"
)
var (
// ErrConflict is used to indicate the upload was rejected
// due to a conflict on the state
ErrConflict = fmt.Errorf("Conflicting state file")
// ErrServerNewer is used to indicate the serial number of
// the state is newer on the server side
ErrServerNewer = fmt.Errorf("Server-side Serial is newer")
// ErrRequireAuth is used if the remote server requires
// authentication and none is provided
ErrRequireAuth = fmt.Errorf("Remote server requires authentication")
// ErrInvalidAuth is used if we provide authentication which
// is not valid
ErrInvalidAuth = fmt.Errorf("Invalid authentication")
// ErrRemoteInternal is used if we get an internal error
// from the remote server
ErrRemoteInternal = fmt.Errorf("Remote server reporting internal error")
)
type RemoteClient interface {
GetState() (*RemoteStatePayload, error)
PutState(state []byte, force bool) error
DeleteState() error
}
// RemoteStatePayload is used to return the remote state
// along with associated meta data when we do a remote fetch.
type RemoteStatePayload struct {
MD5 []byte
State []byte
}
// NewClientByState is used to construct a client from
// our remote state.
func NewClientByState(remote *terraform.RemoteState) (RemoteClient, error) {
return NewClientByType(remote.Type, remote.Config)
}
// NewClientByType is used to construct a RemoteClient
// based on the configured type.
func NewClientByType(ctype string, conf map[string]string) (RemoteClient, error) {
ctype = strings.ToLower(ctype)
switch ctype {
case "atlas":
return NewAtlasRemoteClient(conf)
case "consul":
return NewConsulRemoteClient(conf)
case "http":
return NewHTTPRemoteClient(conf)
default:
return nil, fmt.Errorf("Unknown remote client type '%s'", ctype)
}
}

View File

@ -1 +0,0 @@
package remote

View File

@ -1,77 +0,0 @@
package remote
import (
"crypto/md5"
"fmt"
consulapi "github.com/hashicorp/consul/api"
)
// ConsulRemoteClient implements the RemoteClient interface
// for an Consul compatible server.
type ConsulRemoteClient struct {
client *consulapi.Client
path string // KV path
}
func NewConsulRemoteClient(conf map[string]string) (*ConsulRemoteClient, error) {
client := &ConsulRemoteClient{}
if err := client.validateConfig(conf); err != nil {
return nil, err
}
return client, nil
}
func (c *ConsulRemoteClient) validateConfig(conf map[string]string) (err error) {
config := consulapi.DefaultConfig()
if token, ok := conf["access_token"]; ok && token != "" {
config.Token = token
}
if addr, ok := conf["address"]; ok && addr != "" {
config.Address = addr
}
path, ok := conf["path"]
if !ok || path == "" {
return fmt.Errorf("missing 'path' configuration")
}
c.path = path
c.client, err = consulapi.NewClient(config)
return err
}
func (c *ConsulRemoteClient) GetState() (*RemoteStatePayload, error) {
kv := c.client.KV()
pair, _, err := kv.Get(c.path, nil)
if err != nil {
return nil, err
}
if pair == nil {
return nil, nil
}
// Create the payload
payload := &RemoteStatePayload{
State: pair.Value,
}
// Generate the MD5
hash := md5.Sum(payload.State)
payload.MD5 = hash[:md5.Size]
return payload, nil
}
func (c *ConsulRemoteClient) PutState(state []byte, force bool) error {
pair := &consulapi.KVPair{
Key: c.path,
Value: state,
}
kv := c.client.KV()
_, err := kv.Put(pair, nil)
return err
}
func (c *ConsulRemoteClient) DeleteState() error {
kv := c.client.KV()
_, err := kv.Delete(c.path, nil)
return err
}

View File

@ -1,173 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"os"
"testing"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/terraform/terraform"
)
func TestConsulRemote_Interface(t *testing.T) {
var client interface{} = &ConsulRemoteClient{}
if _, ok := client.(RemoteClient); !ok {
t.Fatalf("does not implement interface")
}
}
func checkConsul(t *testing.T) {
if os.Getenv("CONSUL_ADDR") == "" {
t.SkipNow()
}
}
func TestConsulRemote_Validate(t *testing.T) {
conf := map[string]string{}
if _, err := NewConsulRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["path"] = "test"
if _, err := NewConsulRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestConsulRemote_GetState(t *testing.T) {
checkConsul(t)
type tcase struct {
Path string
Body []byte
ExpectMD5 []byte
ExpectErr string
}
inp := []byte("testing")
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
cases := []*tcase{
&tcase{
Path: "foo",
Body: inp,
ExpectMD5: hash,
},
&tcase{
Path: "none",
},
}
for _, tc := range cases {
if tc.Body != nil {
conf := consulapi.DefaultConfig()
conf.Address = os.Getenv("CONSUL_ADDR")
client, _ := consulapi.NewClient(conf)
pair := &consulapi.KVPair{Key: tc.Path, Value: tc.Body}
client.KV().Put(pair, nil)
}
remote := &terraform.RemoteState{
Type: "consul",
Config: map[string]string{
"address": os.Getenv("CONSUL_ADDR"),
"path": tc.Path,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
payload, err := r.GetState()
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
if tc.ExpectMD5 != nil {
if payload == nil || !bytes.Equal(payload.MD5, tc.ExpectMD5) {
t.Fatalf("bad: %#v", payload)
}
}
if tc.Body != nil {
if !bytes.Equal(payload.State, tc.Body) {
t.Fatalf("bad: %#v", payload)
}
}
}
}
func TestConsulRemote_PutState(t *testing.T) {
checkConsul(t)
path := "foobar"
inp := []byte("testing")
remote := &terraform.RemoteState{
Type: "consul",
Config: map[string]string{
"address": os.Getenv("CONSUL_ADDR"),
"path": path,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.PutState(inp, false)
if err != nil {
t.Fatalf("err: %v", err)
}
conf := consulapi.DefaultConfig()
conf.Address = os.Getenv("CONSUL_ADDR")
client, _ := consulapi.NewClient(conf)
pair, _, err := client.KV().Get(path, nil)
if err != nil {
t.Fatalf("err: %v", err)
}
if !bytes.Equal(pair.Value, inp) {
t.Fatalf("bad value")
}
}
func TestConsulRemote_DeleteState(t *testing.T) {
checkConsul(t)
path := "testdelete"
// Create the state
conf := consulapi.DefaultConfig()
conf.Address = os.Getenv("CONSUL_ADDR")
client, _ := consulapi.NewClient(conf)
pair := &consulapi.KVPair{Key: path, Value: []byte("test")}
client.KV().Put(pair, nil)
remote := &terraform.RemoteState{
Type: "consul",
Config: map[string]string{
"address": os.Getenv("CONSUL_ADDR"),
"path": path,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.DeleteState()
if err != nil {
t.Fatalf("Err: %v", err)
}
pair, _, err = client.KV().Get(path, nil)
if err != nil {
t.Fatalf("Err: %v", err)
}
if pair != nil {
t.Fatalf("state not deleted")
}
}

View File

@ -1,182 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
)
// HTTPRemoteClient implements the RemoteClient interface
// for an HTTP compatible server.
type HTTPRemoteClient struct {
// url is the URL that we GET / POST / DELETE to
url *url.URL
}
func NewHTTPRemoteClient(conf map[string]string) (*HTTPRemoteClient, error) {
client := &HTTPRemoteClient{}
if err := client.validateConfig(conf); err != nil {
return nil, err
}
return client, nil
}
func (c *HTTPRemoteClient) validateConfig(conf map[string]string) error {
urlRaw, ok := conf["address"]
if !ok || urlRaw == "" {
return fmt.Errorf("missing 'address' configuration")
}
url, err := url.Parse(urlRaw)
if err != nil {
return fmt.Errorf("failed to parse url: %v", err)
}
if url.Scheme != "http" && url.Scheme != "https" {
return fmt.Errorf("invalid url: %s", url)
}
c.url = url
return nil
}
func (c *HTTPRemoteClient) GetState() (*RemoteStatePayload, error) {
// Request the url
resp, err := http.Get(c.url.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Handle the common status codes
switch resp.StatusCode {
case http.StatusOK:
// Handled after
case http.StatusNoContent:
return nil, nil
case http.StatusNotFound:
return nil, nil
case http.StatusUnauthorized:
return nil, ErrRequireAuth
case http.StatusForbidden:
return nil, ErrInvalidAuth
case http.StatusInternalServerError:
return nil, ErrRemoteInternal
default:
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
// Read in the body
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, resp.Body); err != nil {
return nil, fmt.Errorf("Failed to read remote state: %v", err)
}
// Create the payload
payload := &RemoteStatePayload{
State: buf.Bytes(),
}
// Check for the MD5
if raw := resp.Header.Get("Content-MD5"); raw != "" {
md5, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
}
payload.MD5 = md5
} else {
// Generate the MD5
hash := md5.Sum(payload.State)
payload.MD5 = hash[:md5.Size]
}
return payload, nil
}
func (c *HTTPRemoteClient) PutState(state []byte, force bool) error {
// Copy the target URL
base := new(url.URL)
*base = *c.url
// Generate the MD5
hash := md5.Sum(state)
b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size])
// Set the force query parameter if needed
if force {
values := base.Query()
values.Set("force", "true")
base.RawQuery = values.Encode()
}
// Make the HTTP client and request
req, err := http.NewRequest("POST", base.String(), bytes.NewReader(state))
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %v", err)
}
// Prepare the request
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-MD5", b64)
req.ContentLength = int64(len(state))
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to upload state: %v", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusConflict:
return ErrConflict
case http.StatusPreconditionFailed:
return ErrServerNewer
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
default:
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
}
func (c *HTTPRemoteClient) DeleteState() error {
// Make the HTTP request
req, err := http.NewRequest("DELETE", c.url.String(), nil)
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %v", err)
}
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to delete state: %v", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
case http.StatusNoContent:
return nil
case http.StatusNotFound:
return nil
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
default:
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
}

View File

@ -1,331 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"io"
"net/http"
"net/http/httptest"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestHTTPRemote_Interface(t *testing.T) {
var client interface{} = &HTTPRemoteClient{}
if _, ok := client.(RemoteClient); !ok {
t.Fatalf("does not implement interface")
}
}
func TestHTTPRemote_Validate(t *testing.T) {
conf := map[string]string{}
if _, err := NewHTTPRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["address"] = ""
if _, err := NewHTTPRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["address"] = "*"
if _, err := NewHTTPRemoteClient(conf); err == nil {
t.Fatalf("expect error")
}
conf["address"] = "http://cool.com"
if _, err := NewHTTPRemoteClient(conf); err != nil {
t.Fatalf("err: %v", err)
}
}
func TestHTTPRemote_GetState(t *testing.T) {
type tcase struct {
Code int
Header http.Header
Body []byte
ExpectMD5 []byte
ExpectErr string
}
inp := []byte("testing")
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
cases := []*tcase{
&tcase{
Code: http.StatusOK,
Body: inp,
ExpectMD5: hash,
},
&tcase{
Code: http.StatusNoContent,
},
&tcase{
Code: http.StatusNotFound,
},
&tcase{
Code: http.StatusInternalServerError,
ExpectErr: "Remote server reporting internal error",
},
&tcase{
Code: 418,
ExpectErr: "Unexpected HTTP response code 418",
},
}
for _, tc := range cases {
cb := func(resp http.ResponseWriter, req *http.Request) {
for k, v := range tc.Header {
resp.Header()[k] = v
}
resp.WriteHeader(tc.Code)
if tc.Body != nil {
resp.Write(tc.Body)
}
}
s := httptest.NewServer(http.HandlerFunc(cb))
defer s.Close()
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": s.URL,
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
payload, err := r.GetState()
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
if tc.ExpectMD5 != nil {
if payload == nil || !bytes.Equal(payload.MD5, tc.ExpectMD5) {
t.Fatalf("bad: %#v", payload)
}
}
if tc.Body != nil {
if !bytes.Equal(payload.State, tc.Body) {
t.Fatalf("bad: %#v", payload)
}
}
}
}
func TestHTTPRemote_PutState(t *testing.T) {
type tcase struct {
Code int
Path string
Header http.Header
Body []byte
ExpectMD5 []byte
Force bool
ExpectErr string
}
inp := []byte("testing")
inpMD5 := md5.Sum(inp)
hash := inpMD5[:16]
cases := []*tcase{
&tcase{
Code: http.StatusOK,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
},
&tcase{
Code: http.StatusOK,
Path: "/foobar?force=true",
Body: inp,
Force: true,
ExpectMD5: hash,
},
&tcase{
Code: http.StatusConflict,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrConflict.Error(),
},
&tcase{
Code: http.StatusPreconditionFailed,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrServerNewer.Error(),
},
&tcase{
Code: http.StatusUnauthorized,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrRequireAuth.Error(),
},
&tcase{
Code: http.StatusForbidden,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrInvalidAuth.Error(),
},
&tcase{
Code: http.StatusInternalServerError,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrRemoteInternal.Error(),
},
&tcase{
Code: 418,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: "Unexpected HTTP response code 418",
},
}
for _, tc := range cases {
cb := func(resp http.ResponseWriter, req *http.Request) {
for k, v := range tc.Header {
resp.Header()[k] = v
}
resp.WriteHeader(tc.Code)
// Verify the body
buf := bytes.NewBuffer(nil)
io.Copy(buf, req.Body)
if !bytes.Equal(buf.Bytes(), tc.Body) {
t.Fatalf("bad body: %v", buf.Bytes())
}
// Verify the path
req.URL.Host = ""
if req.URL.String() != tc.Path {
t.Fatalf("Bad path: %v %v", req.URL.String(), tc.Path)
}
// Verify the content length
if req.ContentLength != int64(len(tc.Body)) {
t.Fatalf("bad content length: %d", req.ContentLength)
}
// Verify the Content-MD5
b64 := req.Header.Get("Content-MD5")
raw, _ := base64.StdEncoding.DecodeString(b64)
if !bytes.Equal(raw, tc.ExpectMD5) {
t.Fatalf("bad md5: %v", raw)
}
}
s := httptest.NewServer(http.HandlerFunc(cb))
defer s.Close()
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": s.URL + "/foobar",
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.PutState(tc.Body, tc.Force)
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
}
}
func TestHTTPRemote_DeleteState(t *testing.T) {
type tcase struct {
Code int
Path string
Header http.Header
ExpectErr string
}
cases := []*tcase{
&tcase{
Code: http.StatusOK,
Path: "/foobar",
},
&tcase{
Code: http.StatusNoContent,
Path: "/foobar",
},
&tcase{
Code: http.StatusNotFound,
Path: "/foobar",
},
&tcase{
Code: http.StatusUnauthorized,
Path: "/foobar",
ExpectErr: ErrRequireAuth.Error(),
},
&tcase{
Code: http.StatusForbidden,
Path: "/foobar",
ExpectErr: ErrInvalidAuth.Error(),
},
&tcase{
Code: http.StatusInternalServerError,
Path: "/foobar",
ExpectErr: ErrRemoteInternal.Error(),
},
&tcase{
Code: 418,
Path: "/foobar",
ExpectErr: "Unexpected HTTP response code 418",
},
}
for _, tc := range cases {
cb := func(resp http.ResponseWriter, req *http.Request) {
for k, v := range tc.Header {
resp.Header()[k] = v
}
resp.WriteHeader(tc.Code)
// Verify the path
req.URL.Host = ""
if req.URL.String() != tc.Path {
t.Fatalf("Bad path: %v %v", req.URL.String(), tc.Path)
}
}
s := httptest.NewServer(http.HandlerFunc(cb))
defer s.Close()
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": s.URL + "/foobar",
},
}
r, err := NewClientByState(remote)
if err != nil {
t.Fatalf("Err: %v", err)
}
err = r.DeleteState()
errStr := ""
if err != nil {
errStr = err.Error()
}
if errStr != tc.ExpectErr {
t.Fatalf("bad err: %v %v", errStr, tc.ExpectErr)
}
}
}

View File

@ -1,448 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"github.com/hashicorp/terraform/terraform"
)
const (
// LocalDirectory is the directory created in the working
// dir to hold the remote state file.
LocalDirectory = ".terraform"
// HiddenStateFile is the name of the state file in the
// LocalDirectory
HiddenStateFile = "terraform.tfstate"
// BackupHiddenStateFile is the path we backup the state
// file to before modifications are made
BackupHiddenStateFile = "terraform.tfstate.backup"
)
// StateChangeResult is used to communicate to a caller
// what actions have been taken when updating a state file
type StateChangeResult int
const (
// StateChangeNoop indicates nothing has happened,
// but that does not indicate an error. Everything is
// just up to date. (Push/Pull)
StateChangeNoop StateChangeResult = iota
// StateChangeInit indicates that there is no local or
// remote state, and that the state was initialized
StateChangeInit
// StateChangeUpdateLocal indicates the local state
// was updated. (Pull)
StateChangeUpdateLocal
// StateChangeUpdateRemote indicates the remote state
// was updated. (Push)
StateChangeUpdateRemote
// StateChangeLocalNewer means the pull was a no-op
// because the local state is newer than that of the
// server. This means a Push should take place. (Pull)
StateChangeLocalNewer
// StateChangeRemoteNewer means the push was a no-op
// because the remote state is newer than that of the
// local state. This means a Pull should take place.
// (Push)
StateChangeRemoteNewer
// StateChangeConflict means that the push or pull
// was a no-op because there is a conflict. This means
// there are multiple state definitions at the same
// serial number with different contents. This requires
// an operator to intervene and resolve the conflict.
// Shame on the user for doing concurrent apply.
// (Push/Pull)
StateChangeConflict
)
func (sc StateChangeResult) String() string {
switch sc {
case StateChangeNoop:
return "Local and remote state in sync"
case StateChangeInit:
return "Local state initialized"
case StateChangeUpdateLocal:
return "Local state updated"
case StateChangeUpdateRemote:
return "Remote state updated"
case StateChangeLocalNewer:
return "Local state is newer than remote state, push required"
case StateChangeRemoteNewer:
return "Remote state is newer than local state, pull required"
case StateChangeConflict:
return "Local and remote state conflict, manual resolution required"
default:
return fmt.Sprintf("Unknown state change type: %d", sc)
}
}
// SuccessfulPull is used to clasify the StateChangeResult for
// a pull operation. This is different by operation, but can be used
// to determine a proper exit code.
func (sc StateChangeResult) SuccessfulPull() bool {
switch sc {
case StateChangeNoop:
return true
case StateChangeInit:
return true
case StateChangeUpdateLocal:
return true
case StateChangeLocalNewer:
return false
case StateChangeConflict:
return false
default:
return false
}
}
// SuccessfulPush is used to clasify the StateChangeResult for
// a push operation. This is different by operation, but can be used
// to determine a proper exit code
func (sc StateChangeResult) SuccessfulPush() bool {
switch sc {
case StateChangeNoop:
return true
case StateChangeUpdateRemote:
return true
case StateChangeRemoteNewer:
return false
case StateChangeConflict:
return false
default:
return false
}
}
// EnsureDirectory is used to make sure the local storage
// directory exists
func EnsureDirectory() error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Failed to get current directory: %v", err)
}
path := filepath.Join(cwd, LocalDirectory)
if err := os.Mkdir(path, 0770); err != nil {
if os.IsExist(err) {
return nil
}
return fmt.Errorf("Failed to make directory '%s': %v", path, err)
}
return nil
}
// HiddenStatePath is used to return the path to the hidden state file,
// should there be one.
// TODO: Rename to LocalStatePath
func HiddenStatePath() (string, error) {
cwd, err := os.Getwd()
if err != nil {
return "", fmt.Errorf("Failed to get current directory: %v", err)
}
path := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
return path, nil
}
// HaveLocalState is used to check if we have a local state file
func HaveLocalState() (bool, error) {
path, err := HiddenStatePath()
if err != nil {
return false, err
}
return ExistsFile(path)
}
// ExistsFile is used to check if a given file exists
func ExistsFile(path string) (bool, error) {
_, err := os.Stat(path)
if err == nil {
return true, nil
}
if os.IsNotExist(err) {
return false, nil
}
return false, err
}
// ValidConfig does a purely logical validation of the remote config
func ValidConfig(conf *terraform.RemoteState) error {
// Default the type to Atlas
if conf.Type == "" {
conf.Type = "atlas"
}
_, err := NewClientByState(conf)
if err != nil {
return err
}
return nil
}
// ReadLocalState is used to read and parse the local state file
func ReadLocalState() (*terraform.State, []byte, error) {
path, err := HiddenStatePath()
if err != nil {
return nil, nil, err
}
// Open the existing file
raw, err := ioutil.ReadFile(path)
if err != nil {
if os.IsNotExist(err) {
return nil, nil, nil
}
return nil, nil, fmt.Errorf("Failed to open state file '%s': %s", path, err)
}
// Decode the state
state, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
return nil, nil, fmt.Errorf("Failed to read state file '%s': %v", path, err)
}
return state, raw, nil
}
// RefreshState is used to read the remote state given
// the configuration for the remote endpoint, and update
// the local state if necessary.
func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) {
if conf == nil {
return StateChangeNoop, fmt.Errorf("Missing remote server configuration")
}
// Read the state from the server
client, err := NewClientByState(conf)
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to create remote client: %v", err)
}
payload, err := client.GetState()
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to read remote state: %v", err)
}
// Parse the remote state
var remoteState *terraform.State
if payload != nil {
remoteState, err = terraform.ReadState(bytes.NewReader(payload.State))
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to parse remote state: %v", err)
}
// Ensure we understand the remote version!
if remoteState.Version > terraform.StateVersion {
return StateChangeNoop, fmt.Errorf(
`Remote state is version %d, this version of Terraform only understands up to %d`, remoteState.Version, terraform.StateVersion)
}
}
// Decode the state
localState, raw, err := ReadLocalState()
if err != nil {
return StateChangeNoop, err
}
// We need to handle the matrix of cases in reconciling
// the local and remote state. Primarily the concern is
// around the Serial number which should grow monotonically.
// Additionally, we use the MD5 to detect a conflict for
// a given Serial.
switch {
case remoteState == nil && localState == nil:
// Initialize a blank state
out, _ := blankState(conf)
if err := Persist(bytes.NewReader(out)); err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to persist state: %v", err)
}
return StateChangeInit, nil
case remoteState == nil && localState != nil:
// User should probably do a push, nothing to do
return StateChangeLocalNewer, nil
case remoteState != nil && localState == nil:
goto PERSIST
case remoteState.Serial < localState.Serial:
// User should probably do a push, nothing to do
return StateChangeLocalNewer, nil
case remoteState.Serial > localState.Serial:
goto PERSIST
case remoteState.Serial == localState.Serial:
// Check for a hash collision on the local/remote state
localMD5 := md5.Sum(raw)
if bytes.Equal(localMD5[:md5.Size], payload.MD5) {
// Hash collision, everything is up-to-date
return StateChangeNoop, nil
} else {
// This is very bad. This means we have 2 state files
// with the same Serial but a different hash. Most probably
// explaination is two parallel apply operations. This
// requires a manual reconciliation.
return StateChangeConflict, nil
}
default:
// We should not reach this point
panic("Unhandled remote update case")
}
PERSIST:
// Update the local state from the remote state
if err := Persist(bytes.NewReader(payload.State)); err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to persist state: %v", err)
}
return StateChangeUpdateLocal, nil
}
// PushState is used to read the local state and
// update the remote state if necessary. The state push
// can be 'forced' to override any conflict detection
// on the server-side.
func PushState(conf *terraform.RemoteState, force bool) (StateChangeResult, error) {
// Read the local state
_, raw, err := ReadLocalState()
if err != nil {
return StateChangeNoop, err
}
// Check if there is no local state
if raw == nil {
return StateChangeNoop, fmt.Errorf("No local state to push")
}
// Push the state to the server
client, err := NewClientByState(conf)
if err != nil {
return StateChangeNoop,
fmt.Errorf("Failed to create remote client: %v", err)
}
err = client.PutState(raw, force)
// Handle the various edge cases
switch err {
case nil:
return StateChangeUpdateRemote, nil
case ErrServerNewer:
return StateChangeRemoteNewer, nil
case ErrConflict:
return StateChangeConflict, nil
default:
return StateChangeNoop, err
}
}
// DeleteState is used to delete the remote state given
// the configuration for the remote endpoint.
func DeleteState(conf *terraform.RemoteState) error {
if conf == nil {
return fmt.Errorf("Missing remote server configuration")
}
// Setup the client
client, err := NewClientByState(conf)
if err != nil {
return fmt.Errorf("Failed to create remote client: %v", err)
}
// Destroy the state
err = client.DeleteState()
if err != nil {
return fmt.Errorf("Failed to delete remote state: %v", err)
}
return nil
}
// blankState is used to return a serialized form of a blank state
// with only the remote info.
func blankState(conf *terraform.RemoteState) ([]byte, error) {
blank := terraform.NewState()
blank.Remote = conf
buf := bytes.NewBuffer(nil)
err := terraform.WriteState(blank, buf)
return buf.Bytes(), err
}
// PersistState is used to persist out the given terraform state
// in our local state cache location.
func PersistState(s *terraform.State) error {
buf := bytes.NewBuffer(nil)
if err := terraform.WriteState(s, buf); err != nil {
return fmt.Errorf("Failed to encode state: %v", err)
}
if err := Persist(buf); err != nil {
return err
}
return nil
}
// Persist is used to write out the state given by a reader (likely
// being streamed from a remote server) to the local storage.
func Persist(r io.Reader) error {
cwd, err := os.Getwd()
if err != nil {
return fmt.Errorf("Failed to get current directory: %v", err)
}
statePath := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
backupPath := filepath.Join(cwd, LocalDirectory, BackupHiddenStateFile)
// Backup the old file if it exists
if err := CopyFile(statePath, backupPath); err != nil {
return fmt.Errorf("Failed to backup state file '%s' to '%s': %v", statePath, backupPath, err)
}
// Open the state path
fh, err := os.Create(statePath)
if err != nil {
return fmt.Errorf("Failed to open state file '%s': %v", statePath, err)
}
// Copy the new state
_, err = io.Copy(fh, r)
fh.Close()
if err != nil {
os.Remove(statePath)
return fmt.Errorf("Failed to persist state file: %v", err)
}
return nil
}
// CopyFile is used to copy from a source file if it exists to a destination.
// This is used to create a backup of the state file.
func CopyFile(src, dst string) error {
srcFH, err := os.Open(src)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
defer srcFH.Close()
dstFH, err := os.Create(dst)
if err != nil {
return err
}
defer dstFH.Close()
_, err = io.Copy(dstFH, srcFH)
return err
}

View File

@ -1,480 +0,0 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"encoding/json"
"io/ioutil"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestEnsureDirectory(t *testing.T) {
err := EnsureDirectory()
if err != nil {
t.Fatalf("Err: %v", err)
}
cwd, _ := os.Getwd()
path := filepath.Join(cwd, LocalDirectory)
_, err = os.Stat(path)
if err != nil {
t.Fatalf("err: %v", err)
}
}
func TestHiddenStatePath(t *testing.T) {
path, err := HiddenStatePath()
if err != nil {
t.Fatalf("err: %v", err)
}
cwd, _ := os.Getwd()
expect := filepath.Join(cwd, LocalDirectory, HiddenStateFile)
if path != expect {
t.Fatalf("bad: %v", path)
}
}
func TestValidConfig(t *testing.T) {
conf := &terraform.RemoteState{
Type: "",
Config: map[string]string{},
}
if err := ValidConfig(conf); err == nil {
t.Fatalf("blank should be not be valid: %v", err)
}
conf.Config["name"] = "hashicorp/test-remote-state"
conf.Config["access_token"] = "abcd"
if err := ValidConfig(conf); err != nil {
t.Fatalf("should be valid")
}
if conf.Type != "atlas" {
t.Fatalf("should default to atlas")
}
}
func TestRefreshState_Init(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemote(t, nil)
defer srv.Close()
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeInit {
t.Fatalf("bad: %s", sc)
}
local := testReadLocal(t)
if !local.Remote.Equals(remote) {
t.Fatalf("Bad: %#v", local)
}
if local.Serial != 1 {
t.Fatalf("Bad: %#v", local)
}
}
func TestRefreshState_NewVersion(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 100
rs.Version = terraform.StateVersion + 1
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 99
testWriteLocal(t, local)
_, err := RefreshState(remote)
if err == nil {
t.Fatalf("New version should fail!")
}
}
func TestRefreshState_Noop(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 100
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 100
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeNoop {
t.Fatalf("bad: %s", sc)
}
}
func TestRefreshState_UpdateLocal(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 100
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 99
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeUpdateLocal {
t.Fatalf("bad: %s", sc)
}
// Should update
local2 := testReadLocal(t)
if local2.Serial != 100 {
t.Fatalf("Bad: %#v", local2)
}
}
func TestRefreshState_LocalNewer(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 99
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 100
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeLocalNewer {
t.Fatalf("bad: %s", sc)
}
}
func TestRefreshState_Conflict(t *testing.T) {
defer testFixCwd(testDir(t))
rs := terraform.NewState()
rs.Serial = 50
rs.RootModule().Outputs["foo"] = "bar"
remote, srv := testRemote(t, rs)
defer srv.Close()
local := terraform.NewState()
local.Serial = 50
local.RootModule().Outputs["foo"] = "baz"
testWriteLocal(t, local)
sc, err := RefreshState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeConflict {
t.Fatalf("bad: %s", sc)
}
}
func TestPushState_NoState(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemotePush(t, 200)
defer srv.Close()
sc, err := PushState(remote, false)
if err.Error() != "No local state to push" {
t.Fatalf("err: %v", err)
}
if sc != StateChangeNoop {
t.Fatalf("Bad: %v", sc)
}
}
func TestPushState_Update(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemotePush(t, 200)
defer srv.Close()
local := terraform.NewState()
testWriteLocal(t, local)
sc, err := PushState(remote, false)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeUpdateRemote {
t.Fatalf("Bad: %v", sc)
}
}
func TestPushState_RemoteNewer(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemotePush(t, 412)
defer srv.Close()
local := terraform.NewState()
testWriteLocal(t, local)
sc, err := PushState(remote, false)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeRemoteNewer {
t.Fatalf("Bad: %v", sc)
}
}
func TestPushState_Conflict(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemotePush(t, 409)
defer srv.Close()
local := terraform.NewState()
testWriteLocal(t, local)
sc, err := PushState(remote, false)
if err != nil {
t.Fatalf("err: %v", err)
}
if sc != StateChangeConflict {
t.Fatalf("Bad: %v", sc)
}
}
func TestPushState_Error(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemotePush(t, 500)
defer srv.Close()
local := terraform.NewState()
testWriteLocal(t, local)
sc, err := PushState(remote, false)
if err != ErrRemoteInternal {
t.Fatalf("err: %v", err)
}
if sc != StateChangeNoop {
t.Fatalf("Bad: %v", sc)
}
}
func TestDeleteState(t *testing.T) {
defer testFixCwd(testDir(t))
remote, srv := testRemotePush(t, 200)
defer srv.Close()
local := terraform.NewState()
testWriteLocal(t, local)
err := DeleteState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
}
func TestBlankState(t *testing.T) {
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": "http://foo.com/",
},
}
r, err := blankState(remote)
if err != nil {
t.Fatalf("err: %v", err)
}
s, err := terraform.ReadState(bytes.NewReader(r))
if err != nil {
t.Fatalf("err: %v", err)
}
if !remote.Equals(s.Remote) {
t.Fatalf("remote mismatch")
}
}
func TestPersist(t *testing.T) {
tmp, cwd := testDir(t)
defer testFixCwd(tmp, cwd)
EnsureDirectory()
// Place old state file, should backup
old := filepath.Join(tmp, LocalDirectory, HiddenStateFile)
ioutil.WriteFile(old, []byte("test"), 0777)
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": "http://foo.com/",
},
}
blank, _ := blankState(remote)
if err := Persist(bytes.NewReader(blank)); err != nil {
t.Fatalf("err: %v", err)
}
// Check for backup
backup := filepath.Join(tmp, LocalDirectory, BackupHiddenStateFile)
out, err := ioutil.ReadFile(backup)
if err != nil {
t.Fatalf("Err: %v", err)
}
if string(out) != "test" {
t.Fatalf("bad: %v", out)
}
// Read the state
out, err = ioutil.ReadFile(old)
if err != nil {
t.Fatalf("Err: %v", err)
}
s, err := terraform.ReadState(bytes.NewReader(out))
if err != nil {
t.Fatalf("Err: %v", err)
}
// Check the remote
if !remote.Equals(s.Remote) {
t.Fatalf("remote mismatch")
}
}
// testRemote is used to make a test HTTP server to
// return a given state file
func testRemote(t *testing.T, s *terraform.State) (*terraform.RemoteState, *httptest.Server) {
var b64md5 string
buf := bytes.NewBuffer(nil)
if s != nil {
enc := json.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
t.Fatalf("err: %v", err)
}
md5 := md5.Sum(buf.Bytes())
b64md5 = base64.StdEncoding.EncodeToString(md5[:16])
}
cb := func(resp http.ResponseWriter, req *http.Request) {
if s == nil {
resp.WriteHeader(404)
return
}
resp.Header().Set("Content-MD5", b64md5)
resp.Write(buf.Bytes())
}
srv := httptest.NewServer(http.HandlerFunc(cb))
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": srv.URL,
},
}
return remote, srv
}
// testRemotePush is used to make a test HTTP server to
// return a given status code on push
func testRemotePush(t *testing.T, c int) (*terraform.RemoteState, *httptest.Server) {
cb := func(resp http.ResponseWriter, req *http.Request) {
resp.WriteHeader(c)
}
srv := httptest.NewServer(http.HandlerFunc(cb))
remote := &terraform.RemoteState{
Type: "http",
Config: map[string]string{
"address": srv.URL,
},
}
return remote, srv
}
// testDir is used to change the current working directory
// into a test directory that should be remoted after
func testDir(t *testing.T) (string, string) {
tmp, err := ioutil.TempDir("", "remote")
if err != nil {
t.Fatalf("err: %v", err)
}
cwd, err := os.Getwd()
if err != nil {
t.Fatalf("err: %v", err)
}
os.Chdir(tmp)
if err := EnsureDirectory(); err != nil {
t.Fatalf("err: %v", err)
}
return tmp, cwd
}
// testFixCwd is used to as a defer to testDir
func testFixCwd(tmp, cwd string) {
os.Chdir(cwd)
os.RemoveAll(tmp)
}
// testReadLocal is used to just get the local state
func testReadLocal(t *testing.T) *terraform.State {
path, err := HiddenStatePath()
if err != nil {
t.Fatalf("err: %v", err)
}
raw, err := ioutil.ReadFile(path)
if err != nil && !os.IsNotExist(err) {
t.Fatalf("err: %v", err)
}
if raw == nil {
return nil
}
s, err := terraform.ReadState(bytes.NewReader(raw))
if err != nil {
t.Fatalf("err: %v", err)
}
return s
}
// testWriteLocal is used to write the local state
func testWriteLocal(t *testing.T, s *terraform.State) {
path, err := HiddenStatePath()
if err != nil {
t.Fatalf("err: %v", err)
}
buf := bytes.NewBuffer(nil)
enc := json.NewEncoder(buf)
if err := enc.Encode(s); err != nil {
t.Fatalf("err: %v", err)
}
err = ioutil.WriteFile(path, buf.Bytes(), 0777)
if err != nil {
t.Fatalf("err: %v", err)
}
}

63
state/backup.go Normal file
View File

@ -0,0 +1,63 @@
package state
import (
"github.com/hashicorp/terraform/terraform"
)
// BackupState wraps a State that backs up the state on the first time that
// a WriteState or PersistState is called.
//
// If Path exists, it will be overwritten.
type BackupState struct {
Real State
Path string
done bool
}
func (s *BackupState) State() *terraform.State {
return s.Real.State()
}
func (s *BackupState) RefreshState() error {
return s.Real.RefreshState()
}
func (s *BackupState) WriteState(state *terraform.State) error {
if !s.done {
if err := s.backup(); err != nil {
return err
}
}
return s.Real.WriteState(state)
}
func (s *BackupState) PersistState() error {
if !s.done {
if err := s.backup(); err != nil {
return err
}
}
return s.Real.PersistState()
}
func (s *BackupState) backup() error {
state := s.Real.State()
if state == nil {
if err := s.Real.RefreshState(); err != nil {
return err
}
state = s.Real.State()
}
ls := &LocalState{Path: s.Path}
if err := ls.WriteState(state); err != nil {
return err
}
s.done = true
return nil
}

29
state/backup_test.go Normal file
View File

@ -0,0 +1,29 @@
package state
import (
"io/ioutil"
"os"
"testing"
)
func TestBackupState(t *testing.T) {
f, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
f.Close()
defer os.Remove(f.Name())
ls := testLocalState(t)
defer os.Remove(ls.Path)
TestState(t, &BackupState{
Real: ls,
Path: f.Name(),
})
if fi, err := os.Stat(f.Name()); err != nil {
t.Fatalf("err: %s", err)
} else if fi.Size() == 0 {
t.Fatalf("bad: %d", fi.Size())
}
}

233
state/cache.go Normal file
View File

@ -0,0 +1,233 @@
package state
import (
"fmt"
"reflect"
"github.com/hashicorp/terraform/terraform"
)
// CacheState is an implementation of the state interfaces that uses
// a StateReadWriter for a local cache.
type CacheState struct {
Cache CacheStateCache
Durable CacheStateDurable
refreshResult CacheRefreshResult
state *terraform.State
}
// StateReader impl.
func (s *CacheState) State() *terraform.State {
return s.state
}
// WriteState will write and persist the state to the cache.
//
// StateWriter impl.
func (s *CacheState) WriteState(state *terraform.State) error {
if err := s.Cache.WriteState(state); err != nil {
return err
}
s.state = state
return s.Cache.PersistState()
}
// RefreshState will refresh both the cache and the durable states. It
// can return a myriad of errors (defined at the top of this file) depending
// on potential conflicts that can occur while doing this.
//
// If the durable state is newer than the local cache, then the local cache
// will be replaced with the durable.
//
// StateRefresher impl.
func (s *CacheState) RefreshState() error {
// Refresh the durable state
if err := s.Durable.RefreshState(); err != nil {
return err
}
// Refresh the cached state
if err := s.Cache.RefreshState(); err != nil {
return err
}
// Handle the matrix of cases that can happen when comparing these
// two states.
cached := s.Cache.State()
durable := s.Durable.State()
switch {
case cached == nil && durable == nil:
// Initialized
s.refreshResult = CacheRefreshInit
case cached != nil && durable == nil:
// Cache is newer than remote. Not a big deal, user can just
// persist to get correct state.
s.refreshResult = CacheRefreshLocalNewer
case cached == nil && durable != nil:
// Cache should be updated since the remote is set but cache isn't
s.refreshResult = CacheRefreshUpdateLocal
case durable.Serial < cached.Serial:
// Cache is newer than remote. Not a big deal, user can just
// persist to get correct state.
s.refreshResult = CacheRefreshLocalNewer
case durable.Serial > cached.Serial:
// Cache should be updated since the remote is newer
s.refreshResult = CacheRefreshUpdateLocal
case durable.Serial == cached.Serial:
// They're supposedly equal, verify.
if reflect.DeepEqual(cached, durable) {
// Hashes are the same, everything is great
s.refreshResult = CacheRefreshNoop
break
}
// This is very bad. This means we have two state files that
// have the same serial but have a different hash. We can't
// reconcile this. The most likely cause is parallel apply
// operations.
s.refreshResult = CacheRefreshConflict
// Return early so we don't updtae the state
return nil
default:
panic("unhandled cache refresh state")
}
if s.refreshResult == CacheRefreshUpdateLocal {
if err := s.Cache.WriteState(durable); err != nil {
s.refreshResult = CacheRefreshNoop
return err
}
if err := s.Cache.PersistState(); err != nil {
s.refreshResult = CacheRefreshNoop
return err
}
}
s.state = cached
return nil
}
// RefreshResult returns the result of the last refresh.
func (s *CacheState) RefreshResult() CacheRefreshResult {
return s.refreshResult
}
// PersistState takes the local cache, assuming it is newer than the remote
// state, and persists it to the durable storage. If you want to challenge the
// assumption that the local state is the latest, call a RefreshState prior
// to this.
//
// StatePersister impl.
func (s *CacheState) PersistState() error {
if err := s.Durable.WriteState(s.state); err != nil {
return err
}
return s.Durable.PersistState()
}
// CacheStateCache is the meta-interface that must be implemented for
// the cache for the CacheState.
type CacheStateCache interface {
StateReader
StateWriter
StatePersister
StateRefresher
}
// CacheStateDurable is the meta-interface that must be implemented for
// the durable storage for CacheState.
type CacheStateDurable interface {
StateReader
StateWriter
StatePersister
StateRefresher
}
// CacheRefreshResult is used to explain the result of the previous
// RefreshState for a CacheState.
type CacheRefreshResult int
const (
// CacheRefreshNoop indicates nothing has happened,
// but that does not indicate an error. Everything is
// just up to date. (Push/Pull)
CacheRefreshNoop CacheRefreshResult = iota
// CacheRefreshInit indicates that there is no local or
// remote state, and that the state was initialized
CacheRefreshInit
// CacheRefreshUpdateLocal indicates the local state
// was updated. (Pull)
CacheRefreshUpdateLocal
// CacheRefreshUpdateRemote indicates the remote state
// was updated. (Push)
CacheRefreshUpdateRemote
// CacheRefreshLocalNewer means the pull was a no-op
// because the local state is newer than that of the
// server. This means a Push should take place. (Pull)
CacheRefreshLocalNewer
// CacheRefreshRemoteNewer means the push was a no-op
// because the remote state is newer than that of the
// local state. This means a Pull should take place.
// (Push)
CacheRefreshRemoteNewer
// CacheRefreshConflict means that the push or pull
// was a no-op because there is a conflict. This means
// there are multiple state definitions at the same
// serial number with different contents. This requires
// an operator to intervene and resolve the conflict.
// Shame on the user for doing concurrent apply.
// (Push/Pull)
CacheRefreshConflict
)
func (sc CacheRefreshResult) String() string {
switch sc {
case CacheRefreshNoop:
return "Local and remote state in sync"
case CacheRefreshInit:
return "Local state initialized"
case CacheRefreshUpdateLocal:
return "Local state updated"
case CacheRefreshUpdateRemote:
return "Remote state updated"
case CacheRefreshLocalNewer:
return "Local state is newer than remote state, push required"
case CacheRefreshRemoteNewer:
return "Remote state is newer than local state, pull required"
case CacheRefreshConflict:
return "Local and remote state conflict, manual resolution required"
default:
return fmt.Sprintf("Unknown state change type: %d", sc)
}
}
// SuccessfulPull is used to clasify the CacheRefreshResult for
// a refresh operation. This is different by operation, but can be used
// to determine a proper exit code.
func (sc CacheRefreshResult) SuccessfulPull() bool {
switch sc {
case CacheRefreshNoop:
return true
case CacheRefreshInit:
return true
case CacheRefreshUpdateLocal:
return true
case CacheRefreshLocalNewer:
return false
case CacheRefreshConflict:
return false
default:
return false
}
}

58
state/cache_test.go Normal file
View File

@ -0,0 +1,58 @@
package state
import (
"os"
"reflect"
"testing"
)
func TestCacheState(t *testing.T) {
cache := testLocalState(t)
durable := testLocalState(t)
defer os.Remove(cache.Path)
defer os.Remove(durable.Path)
TestState(t, &CacheState{
Cache: cache,
Durable: durable,
})
}
func TestCacheState_persistDurable(t *testing.T) {
cache := testLocalState(t)
durable := testLocalState(t)
defer os.Remove(cache.Path)
defer os.Remove(durable.Path)
cs := &CacheState{
Cache: cache,
Durable: durable,
}
state := cache.State()
state.Modules = nil
if err := cs.WriteState(state); err != nil {
t.Fatalf("err: %s", err)
}
if reflect.DeepEqual(cache.State(), durable.State()) {
t.Fatal("cache and durable should not be the same")
}
if err := cs.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
if !reflect.DeepEqual(cache.State(), durable.State()) {
t.Fatalf(
"cache and durable should be the same\n\n%#v\n\n%#v",
cache.State(), durable.State())
}
}
func TestCacheState_impl(t *testing.T) {
var _ StateReader = new(CacheState)
var _ StateWriter = new(CacheState)
var _ StatePersister = new(CacheState)
var _ StateRefresher = new(CacheState)
}

27
state/inmem.go Normal file
View File

@ -0,0 +1,27 @@
package state
import (
"github.com/hashicorp/terraform/terraform"
)
// InmemState is an in-memory state storage.
type InmemState struct {
state *terraform.State
}
func (s *InmemState) State() *terraform.State {
return s.state
}
func (s *InmemState) RefreshState() error {
return nil
}
func (s *InmemState) WriteState(state *terraform.State) error {
s.state = state
return nil
}
func (s *InmemState) PersistState() error {
return nil
}

16
state/inmem_test.go Normal file
View File

@ -0,0 +1,16 @@
package state
import (
"testing"
)
func TestInmemState(t *testing.T) {
TestState(t, &InmemState{state: TestStateInitial()})
}
func TestInmemState_impl(t *testing.T) {
var _ StateReader = new(InmemState)
var _ StateWriter = new(InmemState)
var _ StatePersister = new(InmemState)
var _ StateRefresher = new(InmemState)
}

109
state/local.go Normal file
View File

@ -0,0 +1,109 @@
package state
import (
"os"
"path/filepath"
"github.com/hashicorp/terraform/terraform"
)
// LocalState manages a state storage that is local to the filesystem.
type LocalState struct {
// Path is the path to read the state from. PathOut is the path to
// write the state to. If PathOut is not specified, Path will be used.
// If PathOut already exists, it will be overwritten.
Path string
PathOut string
state *terraform.State
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
}
// WriteState for LocalState always persists the state as well.
//
// StateWriter impl.
func (s *LocalState) WriteState(state *terraform.State) error {
s.state = state
path := s.PathOut
if 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
}
// Create all the directories
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return err
}
f, err := os.Create(path)
if err != nil {
return err
}
defer f.Close()
if err := terraform.WriteState(s.state, f); err != nil {
return err
}
s.written = true
return nil
}
// PersistState for LocalState is a no-op since WriteState always persists.
//
// StatePersister impl.
func (s *LocalState) PersistState() error {
return nil
}
// StateRefresher impl.
func (s *LocalState) RefreshState() error {
// If we've never loaded before, read from Path, otherwise we
// read from PathOut.
path := s.Path
if s.written && s.PathOut != "" {
path = s.PathOut
}
f, err := os.Open(path)
if err != nil {
// It is okay if the file doesn't exist, we treat that as a nil state
if !os.IsNotExist(err) {
return err
}
f = nil
}
var state *terraform.State
if f != nil {
defer f.Close()
state, err = terraform.ReadState(f)
if err != nil {
return err
}
}
s.state = state
return nil
}

68
state/local_test.go Normal file
View File

@ -0,0 +1,68 @@
package state
import (
"io/ioutil"
"os"
"testing"
"github.com/hashicorp/terraform/terraform"
)
func TestLocalState(t *testing.T) {
ls := testLocalState(t)
defer os.Remove(ls.Path)
TestState(t, ls)
}
func TestLocalState_pathOut(t *testing.T) {
f, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
f.Close()
defer os.Remove(f.Name())
ls := testLocalState(t)
ls.PathOut = f.Name()
defer os.Remove(ls.Path)
TestState(t, ls)
}
func TestLocalState_nonExist(t *testing.T) {
ls := &LocalState{Path: "ishouldntexist"}
if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
if state := ls.State(); state != nil {
t.Fatalf("bad: %#v", state)
}
}
func TestLocalState_impl(t *testing.T) {
var _ StateReader = new(LocalState)
var _ StateWriter = new(LocalState)
var _ StatePersister = new(LocalState)
var _ StateRefresher = new(LocalState)
}
func testLocalState(t *testing.T) *LocalState {
f, err := ioutil.TempFile("", "tf")
if err != nil {
t.Fatalf("err: %s", err)
}
err = terraform.WriteState(TestStateInitial(), f)
f.Close()
if err != nil {
t.Fatalf("err: %s", err)
}
ls := &LocalState{Path: f.Name()}
if err := ls.RefreshState(); err != nil {
t.Fatalf("bad: %s", err)
}
return ls
}

View File

@ -18,35 +18,18 @@ const (
defaultAtlasServer = "https://atlas.hashicorp.com/" defaultAtlasServer = "https://atlas.hashicorp.com/"
) )
// AtlasRemoteClient implements the RemoteClient interface func atlasFactory(conf map[string]string) (Client, error) {
// for an Atlas compatible server. var client AtlasClient
type AtlasRemoteClient struct {
server string
serverURL *url.URL
user string
name string
accessToken string
}
func NewAtlasRemoteClient(conf map[string]string) (*AtlasRemoteClient, error) {
client := &AtlasRemoteClient{}
if err := client.validateConfig(conf); err != nil {
return nil, err
}
return client, nil
}
func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error {
server, ok := conf["address"] server, ok := conf["address"]
if !ok || server == "" { if !ok || server == "" {
server = defaultAtlasServer server = defaultAtlasServer
} }
url, err := url.Parse(server) url, err := url.Parse(server)
if err != nil { if err != nil {
return err return nil, err
} }
c.server = server
c.serverURL = url
token, ok := conf["access_token"] token, ok := conf["access_token"]
if token == "" { if token == "" {
@ -54,26 +37,39 @@ func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error {
ok = true ok = true
} }
if !ok || token == "" { if !ok || token == "" {
return fmt.Errorf( return nil, fmt.Errorf(
"missing 'access_token' configuration or ATLAS_TOKEN environmental variable") "missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
} }
c.accessToken = token
name, ok := conf["name"] name, ok := conf["name"]
if !ok || name == "" { if !ok || name == "" {
return fmt.Errorf("missing 'name' configuration") return nil, fmt.Errorf("missing 'name' configuration")
} }
parts := strings.Split(name, "/") parts := strings.Split(name, "/")
if len(parts) != 2 { if len(parts) != 2 {
return fmt.Errorf("malformed name '%s'", name) return nil, fmt.Errorf("malformed name '%s'", name)
}
c.user = parts[0]
c.name = parts[1]
return nil
} }
func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) { client.Server = server
client.ServerURL = url
client.AccessToken = token
client.User = parts[0]
client.Name = parts[1]
return &client, nil
}
// AtlasClient implements the Client interface for an Atlas compatible server.
type AtlasClient struct {
Server string
ServerURL *url.URL
User string
Name string
AccessToken string
}
func (c *AtlasClient) Get() (*Payload, error) {
// Make the HTTP request // Make the HTTP request
req, err := http.NewRequest("GET", c.url().String(), nil) req, err := http.NewRequest("GET", c.url().String(), nil)
if err != nil { if err != nil {
@ -96,11 +92,11 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
case http.StatusNotFound: case http.StatusNotFound:
return nil, nil return nil, nil
case http.StatusUnauthorized: case http.StatusUnauthorized:
return nil, ErrRequireAuth return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
case http.StatusForbidden: case http.StatusForbidden:
return nil, ErrInvalidAuth return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
case http.StatusInternalServerError: case http.StatusInternalServerError:
return nil, ErrRemoteInternal return nil, fmt.Errorf("HTTP remote state internal server error")
default: default:
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
} }
@ -112,8 +108,12 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
} }
// Create the payload // Create the payload
payload := &RemoteStatePayload{ payload := &Payload{
State: buf.Bytes(), Data: buf.Bytes(),
}
if len(payload.Data) == 0 {
return nil, nil
} }
// Check for the MD5 // Check for the MD5
@ -122,18 +122,18 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err) return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
} }
payload.MD5 = md5
payload.MD5 = md5
} else { } else {
// Generate the MD5 // Generate the MD5
hash := md5.Sum(payload.State) hash := md5.Sum(payload.Data)
payload.MD5 = hash[:md5.Size] payload.MD5 = hash[:]
} }
return payload, nil return payload, nil
} }
func (c *AtlasRemoteClient) PutState(state []byte, force bool) error { func (c *AtlasClient) Put(state []byte) error {
// Get the target URL // Get the target URL
base := c.url() base := c.url()
@ -141,12 +141,14 @@ func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
hash := md5.Sum(state) hash := md5.Sum(state)
b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size]) b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size])
/*
// Set the force query parameter if needed // Set the force query parameter if needed
if force { if force {
values := base.Query() values := base.Query()
values.Set("force", "true") values.Set("force", "true")
base.RawQuery = values.Encode() base.RawQuery = values.Encode()
} }
*/
// Make the HTTP client and request // Make the HTTP client and request
req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state)) req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state))
@ -170,22 +172,12 @@ func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
switch resp.StatusCode { switch resp.StatusCode {
case http.StatusOK: case http.StatusOK:
return nil return nil
case http.StatusConflict:
return ErrConflict
case http.StatusPreconditionFailed:
return ErrServerNewer
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
default: default:
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) return fmt.Errorf("HTTP error: %d", resp.StatusCode)
} }
} }
func (c *AtlasRemoteClient) DeleteState() error { func (c *AtlasClient) Delete() error {
// Make the HTTP request // Make the HTTP request
req, err := http.NewRequest("DELETE", c.url().String(), nil) req, err := http.NewRequest("DELETE", c.url().String(), nil)
if err != nil { if err != nil {
@ -207,22 +199,18 @@ func (c *AtlasRemoteClient) DeleteState() error {
return nil return nil
case http.StatusNotFound: case http.StatusNotFound:
return nil return nil
case http.StatusUnauthorized: default:
return ErrRequireAuth return fmt.Errorf("HTTP error: %d", resp.StatusCode)
case http.StatusForbidden:
return ErrInvalidAuth
case http.StatusInternalServerError:
return ErrRemoteInternal
} }
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode) return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
} }
func (c *AtlasRemoteClient) url() *url.URL { func (c *AtlasClient) url() *url.URL {
return &url.URL{ return &url.URL{
Scheme: c.serverURL.Scheme, Scheme: c.ServerURL.Scheme,
Host: c.serverURL.Host, Host: c.ServerURL.Host,
Path: path.Join("api/v1/terraform/state", c.user, c.name), Path: path.Join("api/v1/terraform/state", c.User, c.Name),
RawQuery: fmt.Sprintf("access_token=%s", c.accessToken), RawQuery: fmt.Sprintf("access_token=%s", c.AccessToken),
} }
} }

View File

@ -0,0 +1,32 @@
package remote
import (
"net/http"
"os"
"testing"
)
func TestAtlasClient_impl(t *testing.T) {
var _ Client = new(AtlasClient)
}
func TestAtlasClient(t *testing.T) {
if _, err := http.Get("http://google.com"); err != nil {
t.Skipf("skipping, internet seems to not be available: %s", err)
}
token := os.Getenv("ATLAS_TOKEN")
if token == "" {
t.Skipf("skipping, ATLAS_TOKEN must be set")
}
client, err := atlasFactory(map[string]string{
"access_token": token,
"name": "hashicorp/test-remote-state",
})
if err != nil {
t.Fatalf("bad: %s", err)
}
testClient(t, client)
}

View File

@ -0,0 +1,32 @@
package remote
import (
"crypto/md5"
)
// InmemClient is a Client implementation that stores data in memory.
type InmemClient struct {
Data []byte
MD5 []byte
}
func (c *InmemClient) Get() (*Payload, error) {
return &Payload{
Data: c.Data,
MD5: c.MD5,
}, nil
}
func (c *InmemClient) Put(data []byte) error {
md5 := md5.Sum(data)
c.Data = data
c.MD5 = md5[:]
return nil
}
func (c *InmemClient) Delete() error {
c.Data = nil
c.MD5 = nil
return nil
}

70
state/remote/consul.go Normal file
View File

@ -0,0 +1,70 @@
package remote
import (
"crypto/md5"
"fmt"
consulapi "github.com/hashicorp/consul/api"
)
func consulFactory(conf map[string]string) (Client, error) {
path, ok := conf["path"]
if !ok {
return nil, fmt.Errorf("missing 'path' configuration")
}
config := consulapi.DefaultConfig()
if token, ok := conf["access_token"]; ok && token != "" {
config.Token = token
}
if addr, ok := conf["address"]; ok && addr != "" {
config.Address = addr
}
client, err := consulapi.NewClient(config)
if err != nil {
return nil, err
}
return &ConsulClient{
Client: client,
Path: path,
}, nil
}
// ConsulClient is a remote client that stores data in Consul.
type ConsulClient struct {
Client *consulapi.Client
Path string
}
func (c *ConsulClient) Get() (*Payload, error) {
pair, _, err := c.Client.KV().Get(c.Path, nil)
if err != nil {
return nil, err
}
if pair == nil {
return nil, nil
}
md5 := md5.Sum(pair.Value)
return &Payload{
Data: pair.Value,
MD5: md5[:],
}, nil
}
func (c *ConsulClient) Put(data []byte) error {
kv := c.Client.KV()
_, err := kv.Put(&consulapi.KVPair{
Key: c.Path,
Value: data,
}, nil)
return err
}
func (c *ConsulClient) Delete() error {
kv := c.Client.KV()
_, err := kv.Delete(c.Path, nil)
return err
}

View File

@ -0,0 +1,28 @@
package remote
import (
"fmt"
"net/http"
"testing"
"time"
)
func TestConsulClient_impl(t *testing.T) {
var _ Client = new(ConsulClient)
}
func TestConsulClient(t *testing.T) {
if _, err := http.Get("http://google.com"); err != nil {
t.Skipf("skipping, internet seems to not be available: %s", err)
}
client, err := consulFactory(map[string]string{
"address": "demo.consul.io:80",
"path": fmt.Sprintf("tf-unit/%s", time.Now().String()),
})
if err != nil {
t.Fatalf("bad: %s", err)
}
testClient(t, client)
}

161
state/remote/http.go Normal file
View File

@ -0,0 +1,161 @@
package remote
import (
"bytes"
"crypto/md5"
"encoding/base64"
"fmt"
"io"
"net/http"
"net/url"
)
func httpFactory(conf map[string]string) (Client, error) {
address, ok := conf["address"]
if !ok {
return nil, fmt.Errorf("missing 'address' configuration")
}
url, err := url.Parse(address)
if err != nil {
return nil, fmt.Errorf("failed to parse HTTP URL: %s", err)
}
if url.Scheme != "http" && url.Scheme != "https" {
return nil, fmt.Errorf("address must be HTTP or HTTPS")
}
return &HTTPClient{
URL: url,
}, nil
}
// HTTPClient is a remote client that stores data in Consul.
type HTTPClient struct {
URL *url.URL
}
func (c *HTTPClient) Get() (*Payload, error) {
resp, err := http.Get(c.URL.String())
if err != nil {
return nil, err
}
defer resp.Body.Close()
// Handle the common status codes
switch resp.StatusCode {
case http.StatusOK:
// Handled after
case http.StatusNoContent:
return nil, nil
case http.StatusNotFound:
return nil, nil
case http.StatusUnauthorized:
return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
case http.StatusForbidden:
return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
case http.StatusInternalServerError:
return nil, fmt.Errorf("HTTP remote state internal server error")
default:
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
// Read in the body
buf := bytes.NewBuffer(nil)
if _, err := io.Copy(buf, resp.Body); err != nil {
return nil, fmt.Errorf("Failed to read remote state: %s", err)
}
// Create the payload
payload := &Payload{
Data: buf.Bytes(),
}
// If there was no data, then return nil
if len(payload.Data) == 0 {
return nil, nil
}
// Check for the MD5
if raw := resp.Header.Get("Content-MD5"); raw != "" {
md5, err := base64.StdEncoding.DecodeString(raw)
if err != nil {
return nil, fmt.Errorf(
"Failed to decode Content-MD5 '%s': %s", raw, err)
}
payload.MD5 = md5
} else {
// Generate the MD5
hash := md5.Sum(payload.Data)
payload.MD5 = hash[:]
}
return payload, nil
}
func (c *HTTPClient) Put(data []byte) error {
// Copy the target URL
base := *c.URL
// Generate the MD5
hash := md5.Sum(data)
b64 := base64.StdEncoding.EncodeToString(hash[:])
/*
// Set the force query parameter if needed
if force {
values := base.Query()
values.Set("force", "true")
base.RawQuery = values.Encode()
}
*/
// Make the HTTP client and request
req, err := http.NewRequest("POST", base.String(), bytes.NewReader(data))
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %s", err)
}
// Prepare the request
req.Header.Set("Content-Type", "application/octet-stream")
req.Header.Set("Content-MD5", b64)
req.ContentLength = int64(len(data))
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to upload state: %v", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
default:
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
}
}
func (c *HTTPClient) Delete() error {
// Make the HTTP request
req, err := http.NewRequest("DELETE", c.URL.String(), nil)
if err != nil {
return fmt.Errorf("Failed to make HTTP request: %s", err)
}
// Make the request
resp, err := http.DefaultClient.Do(req)
if err != nil {
return fmt.Errorf("Failed to delete state: %s", err)
}
defer resp.Body.Close()
// Handle the error codes
switch resp.StatusCode {
case http.StatusOK:
return nil
default:
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
}
}

53
state/remote/http_test.go Normal file
View File

@ -0,0 +1,53 @@
package remote
import (
"bytes"
"fmt"
"io"
"net/http"
"net/http/httptest"
"net/url"
"testing"
)
func TestHTTPClient_impl(t *testing.T) {
var _ Client = new(HTTPClient)
}
func TestHTTPClient(t *testing.T) {
handler := new(testHTTPHandler)
ts := httptest.NewServer(http.HandlerFunc(handler.Handle))
defer ts.Close()
url, err := url.Parse(ts.URL)
if err != nil {
t.Fatalf("err: %s", err)
}
client := &HTTPClient{URL: url}
testClient(t, client)
}
type testHTTPHandler struct {
Data []byte
}
func (h *testHTTPHandler) Handle(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "GET":
w.Write(h.Data)
case "POST":
buf := new(bytes.Buffer)
if _, err := io.Copy(buf, r.Body); err != nil {
w.WriteHeader(500)
}
h.Data = buf.Bytes()
case "DELETE":
h.Data = nil
w.WriteHeader(200)
default:
w.WriteHeader(500)
w.Write([]byte(fmt.Sprintf("Unknown method: %s", r.Method)))
}
}

42
state/remote/remote.go Normal file
View File

@ -0,0 +1,42 @@
package remote
import (
"fmt"
)
// Client is the interface that must be implemented for a remote state
// driver. It supports dumb put/get/delete, and the higher level structs
// handle persisting the state properly here.
type Client interface {
Get() (*Payload, error)
Put([]byte) error
Delete() error
}
// Payload is the return value from the remote state storage.
type Payload struct {
MD5 []byte
Data []byte
}
// Factory is the factory function to create a remote client.
type Factory func(map[string]string) (Client, error)
// NewClient returns a new Client with the given type and configuration.
// The client is looked up in the BuiltinClients variable.
func NewClient(t string, conf map[string]string) (Client, error) {
f, ok := BuiltinClients[t]
if !ok {
return nil, fmt.Errorf("unknown remote client type: %s", t)
}
return f(conf)
}
// BuiltinClients is the list of built-in clients that can be used with
// NewClient.
var BuiltinClients = map[string]Factory{
"atlas": atlasFactory,
"consul": consulFactory,
"http": httpFactory,
}

View File

@ -0,0 +1,35 @@
package remote
import (
"bytes"
"testing"
)
// testClient is a generic function to test any client.
func testClient(t *testing.T, c Client) {
data := []byte("foo")
if err := c.Put(data); err != nil {
t.Fatalf("put: %s", err)
}
p, err := c.Get()
if err != nil {
t.Fatalf("get: %s", err)
}
if !bytes.Equal(p.Data, data) {
t.Fatalf("bad: %#v", p)
}
if err := c.Delete(); err != nil {
t.Fatalf("delete: %s", err)
}
p, err = c.Get()
if err != nil {
t.Fatalf("get: %s", err)
}
if p != nil {
t.Fatalf("bad: %#v", p)
}
}

57
state/remote/state.go Normal file
View File

@ -0,0 +1,57 @@
package remote
import (
"bytes"
"github.com/hashicorp/terraform/terraform"
)
// State implements the State interfaces in the state package to handle
// reading and writing the remote state. This State on its own does no
// local caching so every persist will go to the remote storage and local
// writes will go to memory.
type State struct {
Client Client
state *terraform.State
}
// StateReader impl.
func (s *State) State() *terraform.State {
return s.state
}
// StateWriter impl.
func (s *State) WriteState(state *terraform.State) error {
s.state = state
return nil
}
// StateRefresher impl.
func (s *State) RefreshState() error {
payload, err := s.Client.Get()
if err != nil {
return err
}
var state *terraform.State
if payload != nil {
state, err = terraform.ReadState(bytes.NewReader(payload.Data))
if err != nil {
return err
}
}
s.state = state
return nil
}
// StatePersister impl.
func (s *State) PersistState() error {
var buf bytes.Buffer
if err := terraform.WriteState(s.state, &buf); err != nil {
return err
}
return s.Client.Put(buf.Bytes())
}

View File

@ -0,0 +1,24 @@
package remote
import (
"testing"
"github.com/hashicorp/terraform/state"
)
func TestState(t *testing.T) {
s := &State{Client: new(InmemClient)}
s.WriteState(state.TestStateInitial())
if err := s.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
state.TestState(t, s)
}
func TestState_impl(t *testing.T) {
var _ state.StateReader = new(State)
var _ state.StateWriter = new(State)
var _ state.StatePersister = new(State)
var _ state.StateRefresher = new(State)
}

42
state/state.go Normal file
View File

@ -0,0 +1,42 @@
package state
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
// been loaded yet, it is okay for State to return nil.
type StateReader interface {
State() *terraform.State
}
// StateWriter is the interface that must be implemented by something that
// can write a state. Writing the state can be cached or in-memory, as
// full persistence should be implemented by StatePersister.
type StateWriter interface {
WriteState(*terraform.State) error
}
// StateRefresher is the interface that is implemented by something that
// can load a state. This might be refreshing it from a remote location or
// it might simply be reloading it from disk.
type StateRefresher interface {
RefreshState() error
}
// StatePersister is implemented to truly persist a state. Whereas StateWriter
// is allowed to perhaps be caching in memory, PersistState must write the
// state to some durable storage.
type StatePersister interface {
PersistState() error
}

94
state/testing.go Normal file
View File

@ -0,0 +1,94 @@
package state
import (
"bytes"
"reflect"
"testing"
"github.com/hashicorp/terraform/terraform"
)
// TestState is a helper for testing state implementations. It is expected
// that the given implementation is pre-loaded with the TestStateInitial
// state.
func TestState(t *testing.T, s interface{}) {
reader, ok := s.(StateReader)
if !ok {
t.Fatalf("must at least be a StateReader")
}
// If it implements refresh, refresh
if rs, ok := s.(StateRefresher); ok {
if err := rs.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
}
// current will track our current state
current := TestStateInitial()
// Check that the initial state is correct
state := reader.State()
current.Serial = state.Serial
if !reflect.DeepEqual(state, current) {
t.Fatalf("not initial: %#v\n\n%#v", state, current)
}
// Write a new state and verify that we have it
if ws, ok := s.(StateWriter); ok {
current.Modules = append(current.Modules, &terraform.ModuleState{
Path: []string{"root"},
Outputs: map[string]string{
"bar": "baz",
},
})
if err := ws.WriteState(current); err != nil {
t.Fatalf("err: %s", err)
}
if actual := reader.State(); !reflect.DeepEqual(actual, current) {
t.Fatalf("bad: %#v\n\n%#v", actual, current)
}
}
// Test persistence
if ps, ok := s.(StatePersister); ok {
if err := ps.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
// Refresh if we got it
if rs, ok := s.(StateRefresher); ok {
if err := rs.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
}
// Just set the serials the same... Then compare.
actual := reader.State()
actual.Serial = current.Serial
if !reflect.DeepEqual(actual, current) {
t.Fatalf("bad: %#v\n\n%#v", actual, current)
}
}
}
// TestStateInitial is the initial state that a State should have
// for TestState.
func TestStateInitial() *terraform.State {
initial := &terraform.State{
Modules: []*terraform.ModuleState{
&terraform.ModuleState{
Path: []string{"root", "child"},
Outputs: map[string]string{
"foo": "bar",
},
},
},
}
var scratch bytes.Buffer
terraform.WriteState(initial, &scratch)
return initial
}

View File

@ -125,6 +125,31 @@ func (s *State) ModuleOrphans(path []string, c *config.Config) [][]string {
return orphans return orphans
} }
// Empty returns true if the state is empty.
func (s *State) Empty() bool {
if s == nil {
return true
}
return len(s.Modules) == 0
}
// IsRemote returns true if State represents a state that exists and is
// remote.
func (s *State) IsRemote() bool {
if s == nil {
return false
}
if s.Remote == nil {
return false
}
if s.Remote.Type == "" {
return false
}
return true
}
// RootModule returns the ModuleState for the root module // RootModule returns the ModuleState for the root module
func (s *State) RootModule() *ModuleState { func (s *State) RootModule() *ModuleState {
root := s.ModuleByPath(rootModulePath) root := s.ModuleByPath(rootModulePath)
@ -266,7 +291,7 @@ func (r *RemoteState) deepcopy() *RemoteState {
} }
func (r *RemoteState) Empty() bool { func (r *RemoteState) Empty() bool {
return r.Type == "" && len(r.Config) == 0 return r == nil || r.Type == ""
} }
func (r *RemoteState) Equals(other *RemoteState) bool { func (r *RemoteState) Equals(other *RemoteState) bool {

View File

@ -312,6 +312,70 @@ func TestInstanceStateEqual(t *testing.T) {
} }
} }
func TestStateEmpty(t *testing.T) {
cases := []struct {
In *State
Result bool
}{
{
nil,
true,
},
{
&State{},
true,
},
{
&State{
Remote: &RemoteState{Type: "foo"},
},
true,
},
{
&State{
Modules: []*ModuleState{
&ModuleState{},
},
},
false,
},
}
for i, tc := range cases {
if tc.In.Empty() != tc.Result {
t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
}
}
}
func TestStateIsRemote(t *testing.T) {
cases := []struct {
In *State
Result bool
}{
{
nil,
false,
},
{
&State{},
false,
},
{
&State{
Remote: &RemoteState{Type: "foo"},
},
true,
},
}
for i, tc := range cases {
if tc.In.IsRemote() != tc.Result {
t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
}
}
}
func TestInstanceState_MergeDiff(t *testing.T) { func TestInstanceState_MergeDiff(t *testing.T) {
is := InstanceState{ is := InstanceState{
ID: "foo", ID: "foo",