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"
"time"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -408,8 +407,9 @@ func TestApply_plan_remoteState(t *testing.T) {
defer func() { test = true }()
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
if err := remote.EnsureDirectory(); err != nil {
t.Fatalf("err: %v", err)
remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil {
t.Fatalf("err: %s", err)
}
// 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
exists, err := remote.ExistsFile(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %v", err)
}
if exists {
if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil {
t.Fatalf("State path should not exist")
}
// Check for remote state
output, _, err := remote.ReadLocalState()
if err != nil {
t.Fatalf("err: %v", err)
}
if output == nil {
t.Fatalf("missing remote state")
if _, err := os.Stat(remoteStatePath); err != nil {
t.Fatalf("missing remote state: %s", err)
}
}

View File

@ -10,6 +10,9 @@ import (
// Set to true when we're testing
var test bool = false
// DefaultDataDir is the default directory for storing local data.
const DefaultDataDir = ".terraform"
// DefaultStateFilename is the default filename used for the state file.
const DefaultStateFilename = "terraform.tfstate"

View File

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

View File

@ -9,15 +9,15 @@ import (
"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'.
type FlagVar map[string]string
type FlagKV map[string]string
func (v *FlagVar) String() string {
func (v *FlagKV) String() string {
return ""
}
func (v *FlagVar) Set(raw string) error {
func (v *FlagKV) Set(raw string) error {
idx := strings.Index(raw, "=")
if idx == -1 {
return fmt.Errorf("No '=' value in arg: %s", raw)
@ -32,16 +32,16 @@ func (v *FlagVar) Set(raw string) error {
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'
type FlagVarFile map[string]string
type FlagKVFile map[string]string
func (v *FlagVarFile) String() string {
func (v *FlagKVFile) String() string {
return ""
}
func (v *FlagVarFile) Set(raw string) error {
vs, err := loadVarFile(raw)
func (v *FlagKVFile) Set(raw string) error {
vs, err := loadKVFile(raw)
if err != nil {
return err
}
@ -57,7 +57,7 @@ func (v *FlagVarFile) Set(raw string) error {
return nil
}
func loadVarFile(rawPath string) (map[string]string, error) {
func loadKVFile(rawPath string) (map[string]string, error) {
path, err := homedir.Expand(rawPath)
if err != nil {
return nil, fmt.Errorf(

View File

@ -7,11 +7,11 @@ import (
"testing"
)
func TestFlagVar_impl(t *testing.T) {
var _ flag.Value = new(FlagVar)
func TestFlagKV_impl(t *testing.T) {
var _ flag.Value = new(FlagKV)
}
func TestFlagVar(t *testing.T) {
func TestFlagKV(t *testing.T) {
cases := []struct {
Input string
Output map[string]string
@ -43,7 +43,7 @@ func TestFlagVar(t *testing.T) {
}
for _, tc := range cases {
f := new(FlagVar)
f := new(FlagKV)
err := f.Set(tc.Input)
if (err != nil) != tc.Error {
t.Fatalf("bad error. Input: %#v", tc.Input)
@ -56,11 +56,11 @@ func TestFlagVar(t *testing.T) {
}
}
func TestFlagVarFile_impl(t *testing.T) {
var _ flag.Value = new(FlagVarFile)
func TestFlagKVFile_impl(t *testing.T) {
var _ flag.Value = new(FlagKVFile)
}
func TestFlagVarFile(t *testing.T) {
func TestFlagKVFile(t *testing.T) {
inputLibucl := `
foo = "bar"
`
@ -93,7 +93,7 @@ foo = "bar"
t.Fatalf("err: %s", err)
}
f := new(FlagVarFile)
f := new(FlagKVFile)
err := f.Set(path)
if (err != nil) != tc.Error {
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/module"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
)
@ -19,14 +18,12 @@ type InitCommand struct {
}
func (c *InitCommand) Run(args []string) int {
var remoteBackend, remoteAddress, remoteAccessToken, remoteName, remotePath string
var remoteBackend string
args = c.Meta.process(args, false)
remoteConfig := make(map[string]string)
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
cmdFlags.StringVar(&remoteBackend, "backend", "atlas", "")
cmdFlags.StringVar(&remoteAddress, "address", "", "")
cmdFlags.StringVar(&remoteAccessToken, "access-token", "", "")
cmdFlags.StringVar(&remoteName, "name", "", "")
cmdFlags.StringVar(&remotePath, "path", "", "")
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
cmdFlags.Var((*FlagKV)(&remoteConfig), "backend-config", "config")
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
if err := cmdFlags.Parse(args); err != nil {
return 1
@ -92,37 +89,34 @@ func (c *InitCommand) Run(args []string) int {
}
// Handle remote state if configured
if remoteAddress != "" || remoteAccessToken != "" || remoteName != "" || remotePath != "" {
if remoteBackend != "" {
var remoteConf terraform.RemoteState
remoteConf.Type = remoteBackend
remoteConf.Config = map[string]string{
"address": remoteAddress,
"access_token": remoteAccessToken,
"name": remoteName,
"path": remotePath,
}
remoteConf.Config = remoteConfig
// Ensure remote state is not already enabled
haveLocal, err := remote.HaveLocalState()
state, err := c.State()
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
}
if haveLocal {
c.Ui.Error("Remote state is already enabled. Aborting.")
if state != nil {
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
}
// Check if we have the non-managed state file
haveNonManaged, err := remote.ExistsFile(DefaultStateFilename)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err))
if s.IsRemote() {
c.Ui.Error(fmt.Sprintf(
"State file already exists with remote state enabled! 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
}
if haveNonManaged {
c.Ui.Error(fmt.Sprintf("Existing state file '%s' found. Aborting.",
DefaultStateFilename))
return 1
}
// Initialize a blank state file with remote enabled
@ -149,20 +143,11 @@ Usage: terraform init [options] SOURCE [PATH]
Options:
-address=url URL of the remote storage server.
Required for HTTP backend, optional for Atlas and Consul.
-backend=atlas Specifies the type of remote backend. If not
specified, local storage will be used.
-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
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.
-backend-config="k=v" Specifies configuration for the remote storage
backend. This can be specified multiple times.
`
return strings.TrimSpace(helpText)

View File

@ -5,7 +5,6 @@ import (
"path/filepath"
"testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -163,7 +162,7 @@ func TestInit_remoteState(t *testing.T) {
args := []string{
"-backend", "http",
"-address", conf.Config["address"],
"-backend-config", "address=" + conf.Config["address"],
testFixturePath("init"),
tmp,
}
@ -175,9 +174,80 @@ func TestInit_remoteState(t *testing.T) {
t.Fatalf("err: %s", err)
}
path, _ := remote.HiddenStatePath()
_, err := os.Stat(path)
if _, err := os.Stat(filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)); err != nil {
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 {
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"
"fmt"
"io"
"log"
"os"
"path/filepath"
"github.com/hashicorp/terraform/config/module"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
"github.com/mitchellh/colorstring"
@ -24,7 +23,8 @@ type Meta struct {
// State read when calling `Context`. This is available after calling
// `Context`.
state *terraform.State
state state.State
stateResult *StateResult
// This can be set by the command itself to provide extra hooks.
extraHooks []terraform.Hook
@ -41,24 +41,22 @@ type Meta struct {
color bool
oldUi cli.Ui
// useRemoteState is enabled if we are using remote state storage
// This is set when the context is loaded if we read from a remote
// enabled state file.
useRemoteState bool
// The fields below are expected to be set by the command via
// command line flags. See the Apply command for an example.
//
// statePath is the path to the state file. If this is empty, then
// no state will be loaded. It is also okay for this to be a path to
// a file that doesn't exist; it is assumed that this means that there
// is simply no state.
statePath string
//
// stateOutPath is used to override the output path for the state.
// If not provided, the StatePath is used causing the old state to
// be overriden.
stateOutPath string
//
// backupPath is used to backup the state file before writing a modified
// version. It defaults to stateOutPath + DefaultBackupExtention
statePath string
stateOutPath string
backupPath string
}
@ -78,11 +76,6 @@ func (m *Meta) initStatePaths() {
// StateOutPath returns the true output path for the state file
func (m *Meta) StateOutPath() string {
m.initStatePaths()
if m.useRemoteState {
path, _ := remote.HiddenStatePath()
return path
}
return m.stateOutPath
}
@ -106,14 +99,16 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
plan, err := terraform.ReadPlan(f)
f.Close()
if err == nil {
// Check if remote state is enabled, but do not refresh.
// Since a plan is supposed to lock-in the changes, we do not
// attempt a state refresh.
if plan != nil && plan.State != nil && plan.State.Remote != nil && plan.State.Remote.Type != "" {
log.Printf("[INFO] Enabling remote state from plan")
m.useRemoteState = true
// Setup our state
state, statePath, err := StateFromPlan(m.statePath, plan)
if err != nil {
return nil, false, fmt.Errorf("Error loading plan: %s", err)
}
// Set our state
m.state = state
m.stateOutPath = statePath
if len(m.variables) > 0 {
return nil, false, fmt.Errorf(
"You can't set variables with the '-var' or '-var-file' flag\n" +
@ -132,11 +127,10 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
}
// Store the loaded state
state, err := m.loadState()
state, err := m.State()
if err != nil {
return nil, false, err
}
m.state = state
// Load the root module
mod, err := module.NewTreeModule("", copts.Path)
@ -154,7 +148,7 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
}
opts.Module = mod
opts.State = state
opts.State = state.State()
ctx := terraform.NewContext(opts)
return ctx, false, nil
}
@ -175,6 +169,53 @@ func (m *Meta) InputMode() terraform.InputMode {
return mode
}
// State returns the state for this meta.
func (m *Meta) State() (state.State, error) {
if m.state != nil {
return m.state, nil
}
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.
func (m *Meta) UIInput() terraform.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
// the existing state file and respecting path configurations.
func (m *Meta) PersistState(s *terraform.State) error {
if m.useRemoteState {
return m.persistRemoteState(s)
}
return m.persistLocalState(s)
}
// persistRemoteState is used to handle persisting a state file
// when remote state management is enabled
func (m *Meta) persistRemoteState(s *terraform.State) error {
log.Printf("[INFO] Persisting state to local cache")
if err := remote.PersistState(s); err != nil {
if err := m.state.WriteState(s); err != nil {
return err
}
log.Printf("[INFO] Uploading state to remote store")
change, err := remote.PushState(s.Remote, false)
if err != nil {
return err
}
if !change.SuccessfulPush() {
return fmt.Errorf("Failed to upload state: %s", change)
}
return nil
}
// persistLocalState is used to handle persisting a state file
// when remote state management is disabled.
func (m *Meta) persistLocalState(s *terraform.State) error {
m.initStatePaths()
// Create a backup of the state before updating
if m.backupPath != "-" {
log.Printf("[INFO] Writing backup state to: %s", m.backupPath)
if err := remote.CopyFile(m.statePath, m.backupPath); err != nil {
return fmt.Errorf("Failed to backup state: %v", err)
}
}
// Open the new state file
fh, err := os.Create(m.stateOutPath)
if err != nil {
return fmt.Errorf("Failed to open state file: %v", err)
}
defer fh.Close()
// Write out the state
if err := terraform.WriteState(s, fh); err != nil {
return fmt.Errorf("Failed to encode the state: %v", err)
}
return nil
return m.state.PersistState()
}
// Input returns true if we should ask for input for context.
@ -329,11 +269,11 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
func (m *Meta) flagSet(n string) *flag.FlagSet {
f := flag.NewFlagSet(n, flag.ContinueOnError)
f.BoolVar(&m.input, "input", true, "input")
f.Var((*FlagVar)(&m.variables), "var", "variables")
f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file")
f.Var((*FlagKV)(&m.variables), "var", "variables")
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
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.

View File

@ -7,7 +7,6 @@ import (
"reflect"
"testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/terraform"
)
@ -182,156 +181,3 @@ func TestMeta_initStatePaths(t *testing.T) {
t.Fatalf("bad: %#v", m)
}
}
func TestMeta_persistLocal(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
m := new(Meta)
s := terraform.NewState()
if err := m.persistLocalState(s); err != nil {
t.Fatalf("err: %v", err)
}
exists, err := remote.ExistsFile(m.stateOutPath)
if err != nil {
t.Fatalf("err: %v", err)
}
if !exists {
t.Fatalf("state should exist")
}
// Write again, shoudl backup
if err := m.persistLocalState(s); err != nil {
t.Fatalf("err: %v", err)
}
exists, err = remote.ExistsFile(m.backupPath)
if err != nil {
t.Fatalf("err: %v", err)
}
if !exists {
t.Fatalf("backup should exist")
}
}
func TestMeta_persistRemote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
err := remote.EnsureDirectory()
if err != nil {
t.Fatalf("err: %v", err)
}
s := terraform.NewState()
conf, srv := testRemoteState(t, s, 200)
s.Remote = conf
defer srv.Close()
m := new(Meta)
if err := m.persistRemoteState(s); err != nil {
t.Fatalf("err: %v", err)
}
local, _, err := remote.ReadLocalState()
if err != nil {
t.Fatalf("err: %v", err)
}
if local == nil {
t.Fatalf("state should exist")
}
if err := m.persistRemoteState(s); err != nil {
t.Fatalf("err: %v", err)
}
backup := remote.LocalDirectory + "/" + remote.BackupHiddenStateFile
exists, err := remote.ExistsFile(backup)
if err != nil {
t.Fatalf("err: %v", err)
}
if !exists {
t.Fatalf("backup should exist")
}
}
func TestMeta_loadState_remote(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
err := remote.EnsureDirectory()
if err != nil {
t.Fatalf("err: %v", err)
}
s := terraform.NewState()
s.Serial = 1000
conf, srv := testRemoteState(t, s, 200)
s.Remote = conf
defer srv.Close()
s.Serial = 500
if err := remote.PersistState(s); err != nil {
t.Fatalf("err: %v", err)
}
m := new(Meta)
s1, err := m.loadState()
if err != nil {
t.Fatalf("err: %v", err)
}
if s1.Serial < 1000 {
t.Fatalf("Bad: %#v", s1)
}
if !m.useRemoteState {
t.Fatalf("should enable remote")
}
}
func TestMeta_loadState_statePath(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
m := new(Meta)
s := terraform.NewState()
s.Serial = 1000
if err := m.persistLocalState(s); err != nil {
t.Fatalf("err: %v", err)
}
s1, err := m.loadState()
if err != nil {
t.Fatalf("err: %v", err)
}
if s1.Serial < 1000 {
t.Fatalf("Bad: %#v", s1)
}
}
func TestMeta_loadState_conflict(t *testing.T) {
tmp, cwd := testCwd(t)
defer testFixCwd(t, tmp, cwd)
err := remote.EnsureDirectory()
if err != nil {
t.Fatalf("err: %v", err)
}
m := new(Meta)
s := terraform.NewState()
if err := remote.PersistState(s); err != nil {
t.Fatalf("err: %v", err)
}
if err := m.persistLocalState(s); err != nil {
t.Fatalf("err: %v", err)
}
_, err = m.loadState()
if err == nil {
t.Fatalf("should error with conflict")
}
}

View File

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

View File

@ -5,7 +5,7 @@ import (
"fmt"
"strings"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/state"
)
type PullCommand struct {
@ -20,32 +20,50 @@ func (c *PullCommand) Run(args []string) int {
return 1
}
// Recover the local state if any
local, _, err := remote.ReadLocalState()
// Read out our state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
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!")
return 1
}
// Attempt the state refresh
change, err := remote.RefreshState(local.Remote)
if err != nil {
// We need the CacheState structure in order to do anything
var cache *state.CacheState
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(
"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
}
// Use an error exit code if the update was not a success
change := cache.RefreshResult()
if !change.SuccessfulPull() {
c.Ui.Error(fmt.Sprintf("%s", change))
return 1
} else {
c.Ui.Output(fmt.Sprintf("%s", change))
}
return 0
}

View File

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

View File

@ -5,7 +5,7 @@ import (
"fmt"
"strings"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/state"
)
type PushCommand struct {
@ -22,31 +22,52 @@ func (c *PushCommand) Run(args []string) int {
return 1
}
// Check for a remote state file
local, _, err := remote.ReadLocalState()
// Read out our state
s, err := c.State()
if err != nil {
c.Ui.Error(fmt.Sprintf("%s", err))
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
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!")
return 1
}
// Attempt to push the state
change, err := remote.PushState(local.Remote, force)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to push state: %v", err))
// We need the CacheState structure in order to do anything
var cache *state.CacheState
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(
"Failed to extract internal CacheState from remote state.\n" +
"This is an internal error, please report it as a bug."))
return 1
}
// Use an error exit code if the update was not a success
if !change.SuccessfulPush() {
c.Ui.Error(fmt.Sprintf("%s", change))
// Refresh the cache state
if err := cache.Cache.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Failed to refresh from remote state: %s", err))
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
}

View File

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

View File

@ -5,8 +5,6 @@ import (
"log"
"os"
"strings"
"github.com/hashicorp/terraform/remote"
)
// 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
remoteEnabled, err := remote.HaveLocalState()
state, err := c.State()
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
}
// Verify that the state path exists. The "ContextArg" function below
// will actually do this, but we want to provide a richer error message
// if possible.
if !remoteEnabled {
if !state.State().IsRemote() {
if _, err := os.Stat(c.Meta.statePath); err != nil {
if os.IsNotExist(err) {
c.Ui.Error(fmt.Sprintf(
@ -95,14 +93,14 @@ func (c *RefreshCommand) Run(args []string) int {
return 1
}
state, err := ctx.Refresh()
newState, err := ctx.Refresh()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
return 1
}
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))
return 1
}

View File

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

View File

@ -4,9 +4,10 @@ import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"testing"
"github.com/hashicorp/terraform/remote"
"github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/cli"
)
@ -26,11 +27,15 @@ func TestRemote_disable(t *testing.T) {
s = terraform.NewState()
s.Serial = 5
s.Remote = conf
if err := remote.EnsureDirectory(); err != nil {
t.Fatalf("err: %v", err)
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
state := &state.LocalState{Path: statePath}
if err := state.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := remote.PersistState(s); err != nil {
t.Fatalf("err: %v", err)
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
@ -45,23 +50,9 @@ func TestRemote_disable(t *testing.T) {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Local state file should be removed
haveLocal, err := remote.HaveLocalState()
if err != nil {
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")
}
// Local state file should be removed and the local cache should exist
testRemoteLocal(t, true)
testRemoteLocalCache(t, false)
// Check that the state file was updated
raw, _ := ioutil.ReadFile(DefaultStateFilename)
@ -71,11 +62,6 @@ func TestRemote_disable(t *testing.T) {
}
// Ensure we updated
// TODO: Should be 10, but WriteState currently
// increments incorrectly
if newState.Serial != 11 {
t.Fatalf("state file not updated: %#v", newState)
}
if newState.Remote != nil {
t.Fatalf("remote configuration not removed")
}
@ -96,11 +82,15 @@ func TestRemote_disable_noPull(t *testing.T) {
s = terraform.NewState()
s.Serial = 5
s.Remote = conf
if err := remote.EnsureDirectory(); err != nil {
t.Fatalf("err: %v", err)
// Write the state
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
state := &state.LocalState{Path: statePath}
if err := state.WriteState(s); err != nil {
t.Fatalf("err: %s", err)
}
if err := remote.PersistState(s); err != nil {
t.Fatalf("err: %v", err)
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
@ -115,23 +105,9 @@ func TestRemote_disable_noPull(t *testing.T) {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
// Local state file should be removed
haveLocal, err := remote.HaveLocalState()
if err != nil {
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")
}
// Local state file should be removed and the local cache should exist
testRemoteLocal(t, true)
testRemoteLocalCache(t, false)
// Check that the state file was updated
raw, _ := ioutil.ReadFile(DefaultStateFilename)
@ -140,12 +116,6 @@ func TestRemote_disable_noPull(t *testing.T) {
t.Fatalf("err: %v", err)
}
// Ensure we DIDNT updated
// TODO: Should be 5, but WriteState currently increments
// this which is incorrect.
if newState.Serial != 7 {
t.Fatalf("state file updated: %#v", newState)
}
if newState.Remote != nil {
t.Fatalf("remote configuration not removed")
}
@ -178,11 +148,15 @@ func TestRemote_disable_otherState(t *testing.T) {
// Persist local remote state
s := terraform.NewState()
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 {
t.Fatalf("err: %v", err)
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
// Also put a file at the default path
@ -218,11 +192,15 @@ func TestRemote_managedAndNonManaged(t *testing.T) {
// Persist local remote state
s := terraform.NewState()
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 {
t.Fatalf("err: %v", err)
if err := state.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
// Also put a file at the default path
@ -265,18 +243,20 @@ func TestRemote_initBlank(t *testing.T) {
args := []string{
"-backend=http",
"-address", "http://example.com",
"-access-token=test",
"-backend-config", "address=http://example.com",
"-backend-config", "access_token=test",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
local, _, err := remote.ReadLocalState()
if err != nil {
t.Fatalf("err: %v", err)
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
ls := &state.LocalState{Path: remotePath}
if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
local := ls.State()
if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote)
}
@ -318,11 +298,15 @@ func TestRemote_updateRemote(t *testing.T) {
s.Remote = &terraform.RemoteState{
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 {
t.Fatalf("err: %v", err)
if err := ls.PersistState(); err != nil {
t.Fatalf("err: %s", err)
}
ui := new(cli.MockUi)
@ -335,18 +319,19 @@ func TestRemote_updateRemote(t *testing.T) {
args := []string{
"-backend=http",
"-address",
"http://example.com",
"-access-token=test",
"-backend-config", "address=http://example.com",
"-backend-config", "access_token=test",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
local, _, err := remote.ReadLocalState()
if err != nil {
t.Fatalf("err: %v", err)
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
ls = &state.LocalState{Path: remotePath}
if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
local := ls.State()
if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote)
@ -389,18 +374,19 @@ func TestRemote_enableRemote(t *testing.T) {
args := []string{
"-backend=http",
"-address",
"http://example.com",
"-access-token=test",
"-backend-config", "address=http://example.com",
"-backend-config", "access_token=test",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
}
local, _, err := remote.ReadLocalState()
if err != nil {
t.Fatalf("err: %v", err)
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
ls := &state.LocalState{Path: remotePath}
if err := ls.RefreshState(); err != nil {
t.Fatalf("err: %s", err)
}
local := ls.State()
if local.Remote.Type != "http" {
t.Fatalf("Bad: %#v", local.Remote)
@ -412,21 +398,49 @@ func TestRemote_enableRemote(t *testing.T) {
t.Fatalf("Bad: %#v", local.Remote)
}
// Backup file should exist
exist, err := remote.ExistsFile(DefaultStateFilename + DefaultBackupExtention)
if err != nil {
t.Fatalf("err: %v", err)
}
if !exist {
t.Fatalf("backup should exist")
// Backup file should exist, state file should not
testRemoteLocal(t, false)
testRemoteLocalBackup(t, true)
}
// State file should not
exist, err = remote.ExistsFile(DefaultStateFilename)
if err != nil {
t.Fatalf("err: %v", err)
func testRemoteLocal(t *testing.T, exists bool) {
_, err := os.Stat(DefaultStateFilename)
if os.IsNotExist(err) && !exists {
return
}
if exist {
t.Fatalf("state file should not exist")
if err == nil && exists {
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"
"strings"
statelib "github.com/hashicorp/terraform/state"
"github.com/hashicorp/terraform/terraform"
)
@ -36,7 +37,7 @@ func (c *ShowCommand) Run(args []string) int {
return 1
}
var err, planErr, stateErr error
var planErr, stateErr error
var path string
var plan *terraform.Plan
var state *terraform.State
@ -68,12 +69,13 @@ func (c *ShowCommand) Run(args []string) int {
} else {
// We should use the default state if it exists.
c.Meta.statePath = DefaultStateFilename
state, err = c.Meta.loadState()
if err != nil {
stateStore := &statelib.LocalState{Path: DefaultStateFilename}
if err := stateStore.RefreshState(); err != nil {
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
return 1
}
state = stateStore.State()
if state == nil {
c.Ui.Output("No state.")
return 0

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.
var Ui cli.Ui
const ErrorPrefix = "e:"
const OutputPrefix = "o:"
const (
ErrorPrefix = "e:"
OutputPrefix = "o:"
)
func init() {
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/"
)
// AtlasRemoteClient implements the RemoteClient interface
// for an Atlas compatible server.
type AtlasRemoteClient struct {
server string
serverURL *url.URL
user string
name string
accessToken string
}
func atlasFactory(conf map[string]string) (Client, error) {
var client AtlasClient
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"]
if !ok || server == "" {
server = defaultAtlasServer
}
url, err := url.Parse(server)
if err != nil {
return err
return nil, err
}
c.server = server
c.serverURL = url
token, ok := conf["access_token"]
if token == "" {
@ -54,26 +37,39 @@ func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error {
ok = true
}
if !ok || token == "" {
return fmt.Errorf(
return nil, fmt.Errorf(
"missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
}
c.accessToken = token
name, ok := conf["name"]
if !ok || name == "" {
return fmt.Errorf("missing 'name' configuration")
return nil, fmt.Errorf("missing 'name' configuration")
}
parts := strings.Split(name, "/")
if len(parts) != 2 {
return fmt.Errorf("malformed name '%s'", name)
}
c.user = parts[0]
c.name = parts[1]
return nil
return nil, fmt.Errorf("malformed name '%s'", name)
}
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
req, err := http.NewRequest("GET", c.url().String(), nil)
if err != nil {
@ -96,11 +92,11 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
case http.StatusNotFound:
return nil, nil
case http.StatusUnauthorized:
return nil, ErrRequireAuth
return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
case http.StatusForbidden:
return nil, ErrInvalidAuth
return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
case http.StatusInternalServerError:
return nil, ErrRemoteInternal
return nil, fmt.Errorf("HTTP remote state internal server error")
default:
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
}
@ -112,8 +108,12 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
}
// Create the payload
payload := &RemoteStatePayload{
State: buf.Bytes(),
payload := &Payload{
Data: buf.Bytes(),
}
if len(payload.Data) == 0 {
return nil, nil
}
// Check for the MD5
@ -122,18 +122,18 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
if err != nil {
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
}
payload.MD5 = md5
payload.MD5 = md5
} else {
// Generate the MD5
hash := md5.Sum(payload.State)
payload.MD5 = hash[:md5.Size]
hash := md5.Sum(payload.Data)
payload.MD5 = hash[:]
}
return payload, nil
}
func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
func (c *AtlasClient) Put(state []byte) error {
// Get the target URL
base := c.url()
@ -141,12 +141,14 @@ func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
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("PUT", base.String(), bytes.NewReader(state))
@ -170,22 +172,12 @@ func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
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)
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
}
}
func (c *AtlasRemoteClient) DeleteState() error {
func (c *AtlasClient) Delete() error {
// Make the HTTP request
req, err := http.NewRequest("DELETE", c.url().String(), nil)
if err != nil {
@ -207,22 +199,18 @@ func (c *AtlasRemoteClient) DeleteState() error {
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("HTTP error: %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{
Scheme: c.serverURL.Scheme,
Host: c.serverURL.Host,
Path: path.Join("api/v1/terraform/state", c.user, c.name),
RawQuery: fmt.Sprintf("access_token=%s", c.accessToken),
Scheme: c.ServerURL.Scheme,
Host: c.ServerURL.Host,
Path: path.Join("api/v1/terraform/state", c.User, c.Name),
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
}
// 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
func (s *State) RootModule() *ModuleState {
root := s.ModuleByPath(rootModulePath)
@ -266,7 +291,7 @@ func (r *RemoteState) deepcopy() *RemoteState {
}
func (r *RemoteState) Empty() bool {
return r.Type == "" && len(r.Config) == 0
return r == nil || r.Type == ""
}
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) {
is := InstanceState{
ID: "foo",