Merge pull request #1028 from hashicorp/f-continuous-state
core: state management refactor to prepare for continuous state
This commit is contained in:
commit
41750dfa05
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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(
|
|
@ -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)
|
|
@ -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.")
|
||||
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))
|
||||
return 1
|
||||
}
|
||||
if haveNonManaged {
|
||||
c.Ui.Error(fmt.Sprintf("Existing state file '%s' found. Aborting.",
|
||||
DefaultStateFilename))
|
||||
return 1
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
|
@ -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 != nil {
|
||||
t.Fatalf("missing state")
|
||||
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("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())
|
||||
}
|
||||
}
|
||||
|
|
206
command/meta.go
206
command/meta.go
|
@ -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,25 +41,23 @@ 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
|
||||
backupPath string
|
||||
statePath string
|
||||
stateOutPath string
|
||||
backupPath string
|
||||
}
|
||||
|
||||
// initStatePaths is used to initialize the default values for
|
||||
|
@ -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.
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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" +
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
@ -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{
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,15 +323,9 @@ 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.
|
||||
before disabling.
|
||||
|
||||
-state=path Path to read state. Defaults to "terraform.tfstate"
|
||||
unless remote state is enabled.
|
||||
|
|
|
@ -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)
|
||||
// Backup file should exist, state file should not
|
||||
testRemoteLocal(t, false)
|
||||
testRemoteLocalBackup(t, true)
|
||||
}
|
||||
|
||||
func testRemoteLocal(t *testing.T, exists bool) {
|
||||
_, err := os.Stat(DefaultStateFilename)
|
||||
if os.IsNotExist(err) && !exists {
|
||||
return
|
||||
}
|
||||
if !exist {
|
||||
t.Fatalf("backup should exist")
|
||||
if err == nil && exists {
|
||||
return
|
||||
}
|
||||
|
||||
// State file should not
|
||||
exist, err = remote.ExistsFile(DefaultStateFilename)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if exist {
|
||||
t.Fatalf("state file should not exist")
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
}
|
|
@ -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{
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
package remote
|
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
182
remote/http.go
182
remote/http.go
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
448
remote/remote.go
448
remote/remote.go
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
return nil, fmt.Errorf("malformed name '%s'", name)
|
||||
}
|
||||
c.user = parts[0]
|
||||
c.name = parts[1]
|
||||
return nil
|
||||
|
||||
client.Server = server
|
||||
client.ServerURL = url
|
||||
client.AccessToken = token
|
||||
client.User = parts[0]
|
||||
client.Name = parts[1]
|
||||
|
||||
return &client, nil
|
||||
}
|
||||
|
||||
func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
|
||||
// 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()
|
||||
}
|
||||
/*
|
||||
// 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),
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue