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"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -408,8 +407,9 @@ func TestApply_plan_remoteState(t *testing.T) {
|
||||||
defer func() { test = true }()
|
defer func() { test = true }()
|
||||||
tmp, cwd := testCwd(t)
|
tmp, cwd := testCwd(t)
|
||||||
defer testFixCwd(t, tmp, cwd)
|
defer testFixCwd(t, tmp, cwd)
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
remoteStatePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
t.Fatalf("err: %v", err)
|
if err := os.MkdirAll(filepath.Dir(remoteStatePath), 0755); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set some default reader/writers for the inputs
|
// Set some default reader/writers for the inputs
|
||||||
|
@ -448,21 +448,13 @@ func TestApply_plan_remoteState(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// State file should be not be installed
|
// State file should be not be installed
|
||||||
exists, err := remote.ExistsFile(DefaultStateFilename)
|
if _, err := os.Stat(filepath.Join(tmp, DefaultStateFilename)); err == nil {
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if exists {
|
|
||||||
t.Fatalf("State path should not exist")
|
t.Fatalf("State path should not exist")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for remote state
|
// Check for remote state
|
||||||
output, _, err := remote.ReadLocalState()
|
if _, err := os.Stat(remoteStatePath); err != nil {
|
||||||
if err != nil {
|
t.Fatalf("missing remote state: %s", err)
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if output == nil {
|
|
||||||
t.Fatalf("missing remote state")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,9 @@ import (
|
||||||
// Set to true when we're testing
|
// Set to true when we're testing
|
||||||
var test bool = false
|
var test bool = false
|
||||||
|
|
||||||
|
// DefaultDataDir is the default directory for storing local data.
|
||||||
|
const DefaultDataDir = ".terraform"
|
||||||
|
|
||||||
// DefaultStateFilename is the default filename used for the state file.
|
// DefaultStateFilename is the default filename used for the state file.
|
||||||
const DefaultStateFilename = "terraform.tfstate"
|
const DefaultStateFilename = "terraform.tfstate"
|
||||||
|
|
||||||
|
|
|
@ -178,17 +178,20 @@ func testTempDir(t *testing.T) string {
|
||||||
// testCwdDir is used to change the current working directory
|
// testCwdDir is used to change the current working directory
|
||||||
// into a test directory that should be remoted after
|
// into a test directory that should be remoted after
|
||||||
func testCwd(t *testing.T) (string, string) {
|
func testCwd(t *testing.T) (string, string) {
|
||||||
tmp, err := ioutil.TempDir("", "remote")
|
tmp, err := ioutil.TempDir("", "tf")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cwd, err := os.Getwd()
|
cwd, err := os.Getwd()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.Chdir(tmp); err != nil {
|
if err := os.Chdir(tmp); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tmp, cwd
|
return tmp, cwd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -197,6 +200,7 @@ func testFixCwd(t *testing.T, tmp, cwd string) {
|
||||||
if err := os.Chdir(cwd); err != nil {
|
if err := os.Chdir(cwd); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := os.RemoveAll(tmp); err != nil {
|
if err := os.RemoveAll(tmp); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,15 +9,15 @@ import (
|
||||||
"github.com/mitchellh/go-homedir"
|
"github.com/mitchellh/go-homedir"
|
||||||
)
|
)
|
||||||
|
|
||||||
// FlagVar is a flag.Value implementation for parsing user variables
|
// FlagKV is a flag.Value implementation for parsing user variables
|
||||||
// from the command-line in the format of '-var key=value'.
|
// from the command-line in the format of '-var key=value'.
|
||||||
type FlagVar map[string]string
|
type FlagKV map[string]string
|
||||||
|
|
||||||
func (v *FlagVar) String() string {
|
func (v *FlagKV) String() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FlagVar) Set(raw string) error {
|
func (v *FlagKV) Set(raw string) error {
|
||||||
idx := strings.Index(raw, "=")
|
idx := strings.Index(raw, "=")
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
return fmt.Errorf("No '=' value in arg: %s", raw)
|
return fmt.Errorf("No '=' value in arg: %s", raw)
|
||||||
|
@ -32,16 +32,16 @@ func (v *FlagVar) Set(raw string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// FlagVarFile is a flag.Value implementation for parsing user variables
|
// FlagKVFile is a flag.Value implementation for parsing user variables
|
||||||
// from the command line in the form of files. i.e. '-var-file=foo'
|
// from the command line in the form of files. i.e. '-var-file=foo'
|
||||||
type FlagVarFile map[string]string
|
type FlagKVFile map[string]string
|
||||||
|
|
||||||
func (v *FlagVarFile) String() string {
|
func (v *FlagKVFile) String() string {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *FlagVarFile) Set(raw string) error {
|
func (v *FlagKVFile) Set(raw string) error {
|
||||||
vs, err := loadVarFile(raw)
|
vs, err := loadKVFile(raw)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -57,7 +57,7 @@ func (v *FlagVarFile) Set(raw string) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadVarFile(rawPath string) (map[string]string, error) {
|
func loadKVFile(rawPath string) (map[string]string, error) {
|
||||||
path, err := homedir.Expand(rawPath)
|
path, err := homedir.Expand(rawPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf(
|
return nil, fmt.Errorf(
|
|
@ -7,11 +7,11 @@ import (
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestFlagVar_impl(t *testing.T) {
|
func TestFlagKV_impl(t *testing.T) {
|
||||||
var _ flag.Value = new(FlagVar)
|
var _ flag.Value = new(FlagKV)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagVar(t *testing.T) {
|
func TestFlagKV(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
Input string
|
Input string
|
||||||
Output map[string]string
|
Output map[string]string
|
||||||
|
@ -43,7 +43,7 @@ func TestFlagVar(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
f := new(FlagVar)
|
f := new(FlagKV)
|
||||||
err := f.Set(tc.Input)
|
err := f.Set(tc.Input)
|
||||||
if (err != nil) != tc.Error {
|
if (err != nil) != tc.Error {
|
||||||
t.Fatalf("bad error. Input: %#v", tc.Input)
|
t.Fatalf("bad error. Input: %#v", tc.Input)
|
||||||
|
@ -56,11 +56,11 @@ func TestFlagVar(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagVarFile_impl(t *testing.T) {
|
func TestFlagKVFile_impl(t *testing.T) {
|
||||||
var _ flag.Value = new(FlagVarFile)
|
var _ flag.Value = new(FlagKVFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFlagVarFile(t *testing.T) {
|
func TestFlagKVFile(t *testing.T) {
|
||||||
inputLibucl := `
|
inputLibucl := `
|
||||||
foo = "bar"
|
foo = "bar"
|
||||||
`
|
`
|
||||||
|
@ -93,7 +93,7 @@ foo = "bar"
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
f := new(FlagVarFile)
|
f := new(FlagKVFile)
|
||||||
err := f.Set(path)
|
err := f.Set(path)
|
||||||
if (err != nil) != tc.Error {
|
if (err != nil) != tc.Error {
|
||||||
t.Fatalf("bad error. Input: %#v", tc.Input)
|
t.Fatalf("bad error. Input: %#v", tc.Input)
|
|
@ -8,7 +8,6 @@ import (
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config"
|
"github.com/hashicorp/terraform/config"
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -19,14 +18,12 @@ type InitCommand struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *InitCommand) Run(args []string) int {
|
func (c *InitCommand) Run(args []string) int {
|
||||||
var remoteBackend, remoteAddress, remoteAccessToken, remoteName, remotePath string
|
var remoteBackend string
|
||||||
args = c.Meta.process(args, false)
|
args = c.Meta.process(args, false)
|
||||||
|
remoteConfig := make(map[string]string)
|
||||||
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
|
cmdFlags := flag.NewFlagSet("init", flag.ContinueOnError)
|
||||||
cmdFlags.StringVar(&remoteBackend, "backend", "atlas", "")
|
cmdFlags.StringVar(&remoteBackend, "backend", "", "")
|
||||||
cmdFlags.StringVar(&remoteAddress, "address", "", "")
|
cmdFlags.Var((*FlagKV)(&remoteConfig), "backend-config", "config")
|
||||||
cmdFlags.StringVar(&remoteAccessToken, "access-token", "", "")
|
|
||||||
cmdFlags.StringVar(&remoteName, "name", "", "")
|
|
||||||
cmdFlags.StringVar(&remotePath, "path", "", "")
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
|
@ -92,37 +89,34 @@ func (c *InitCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle remote state if configured
|
// Handle remote state if configured
|
||||||
if remoteAddress != "" || remoteAccessToken != "" || remoteName != "" || remotePath != "" {
|
if remoteBackend != "" {
|
||||||
var remoteConf terraform.RemoteState
|
var remoteConf terraform.RemoteState
|
||||||
remoteConf.Type = remoteBackend
|
remoteConf.Type = remoteBackend
|
||||||
remoteConf.Config = map[string]string{
|
remoteConf.Config = remoteConfig
|
||||||
"address": remoteAddress,
|
|
||||||
"access_token": remoteAccessToken,
|
|
||||||
"name": remoteName,
|
|
||||||
"path": remotePath,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure remote state is not already enabled
|
state, err := c.State()
|
||||||
haveLocal, err := remote.HaveLocalState()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err))
|
c.Ui.Error(fmt.Sprintf("Error checking for state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if haveLocal {
|
if state != nil {
|
||||||
c.Ui.Error("Remote state is already enabled. Aborting.")
|
s := state.State()
|
||||||
return 1
|
if !s.Empty() {
|
||||||
}
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"State file already exists and is not empty! Please remove this\n" +
|
||||||
// Check if we have the non-managed state file
|
"state file before initializing. Note that removing the state file\n" +
|
||||||
haveNonManaged, err := remote.ExistsFile(DefaultStateFilename)
|
"may result in a loss of information since Terraform uses this\n" +
|
||||||
if err != nil {
|
"to track your infrastructure."))
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err))
|
return 1
|
||||||
return 1
|
}
|
||||||
}
|
if s.IsRemote() {
|
||||||
if haveNonManaged {
|
c.Ui.Error(fmt.Sprintf(
|
||||||
c.Ui.Error(fmt.Sprintf("Existing state file '%s' found. Aborting.",
|
"State file already exists with remote state enabled! Please remove this\n" +
|
||||||
DefaultStateFilename))
|
"state file before initializing. Note that removing the state file\n" +
|
||||||
return 1
|
"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
|
// Initialize a blank state file with remote enabled
|
||||||
|
@ -149,20 +143,11 @@ Usage: terraform init [options] SOURCE [PATH]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-address=url URL of the remote storage server.
|
-backend=atlas Specifies the type of remote backend. If not
|
||||||
Required for HTTP backend, optional for Atlas and Consul.
|
specified, local storage will be used.
|
||||||
|
|
||||||
-access-token=token Authentication token for state storage server.
|
-backend-config="k=v" Specifies configuration for the remote storage
|
||||||
Required for Atlas backend, optional for Consul.
|
backend. This can be specified multiple times.
|
||||||
|
|
||||||
-backend=atlas Specifies the type of remote backend. Must be one
|
|
||||||
of Atlas, Consul, or HTTP. Defaults to atlas.
|
|
||||||
|
|
||||||
-name=name Name of the state file in the state storage server.
|
|
||||||
Required for Atlas backend.
|
|
||||||
|
|
||||||
-path=path Path of the remote state in Consul. Required for the
|
|
||||||
Consul backend.
|
|
||||||
|
|
||||||
`
|
`
|
||||||
return strings.TrimSpace(helpText)
|
return strings.TrimSpace(helpText)
|
||||||
|
|
|
@ -5,7 +5,6 @@ import (
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -163,7 +162,7 @@ func TestInit_remoteState(t *testing.T) {
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-backend", "http",
|
"-backend", "http",
|
||||||
"-address", conf.Config["address"],
|
"-backend-config", "address=" + conf.Config["address"],
|
||||||
testFixturePath("init"),
|
testFixturePath("init"),
|
||||||
tmp,
|
tmp,
|
||||||
}
|
}
|
||||||
|
@ -175,9 +174,80 @@ func TestInit_remoteState(t *testing.T) {
|
||||||
t.Fatalf("err: %s", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
path, _ := remote.HiddenStatePath()
|
if _, err := os.Stat(filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)); err != nil {
|
||||||
_, err := os.Stat(path)
|
t.Fatalf("missing state: %s", err)
|
||||||
if err != nil {
|
}
|
||||||
t.Fatalf("missing state")
|
}
|
||||||
|
|
||||||
|
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"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"log"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/config/module"
|
"github.com/hashicorp/terraform/config/module"
|
||||||
"github.com/hashicorp/terraform/remote"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
"github.com/mitchellh/colorstring"
|
"github.com/mitchellh/colorstring"
|
||||||
|
@ -24,7 +23,8 @@ type Meta struct {
|
||||||
|
|
||||||
// State read when calling `Context`. This is available after calling
|
// State read when calling `Context`. This is available after calling
|
||||||
// `Context`.
|
// `Context`.
|
||||||
state *terraform.State
|
state state.State
|
||||||
|
stateResult *StateResult
|
||||||
|
|
||||||
// This can be set by the command itself to provide extra hooks.
|
// This can be set by the command itself to provide extra hooks.
|
||||||
extraHooks []terraform.Hook
|
extraHooks []terraform.Hook
|
||||||
|
@ -41,25 +41,23 @@ type Meta struct {
|
||||||
color bool
|
color bool
|
||||||
oldUi cli.Ui
|
oldUi cli.Ui
|
||||||
|
|
||||||
// useRemoteState is enabled if we are using remote state storage
|
// The fields below are expected to be set by the command via
|
||||||
// This is set when the context is loaded if we read from a remote
|
// command line flags. See the Apply command for an example.
|
||||||
// enabled state file.
|
//
|
||||||
useRemoteState bool
|
|
||||||
|
|
||||||
// statePath is the path to the state file. If this is empty, then
|
// statePath is the path to the state file. If this is empty, then
|
||||||
// no state will be loaded. It is also okay for this to be a path to
|
// no state will be loaded. It is also okay for this to be a path to
|
||||||
// a file that doesn't exist; it is assumed that this means that there
|
// a file that doesn't exist; it is assumed that this means that there
|
||||||
// is simply no state.
|
// is simply no state.
|
||||||
statePath string
|
//
|
||||||
|
|
||||||
// stateOutPath is used to override the output path for the state.
|
// stateOutPath is used to override the output path for the state.
|
||||||
// If not provided, the StatePath is used causing the old state to
|
// If not provided, the StatePath is used causing the old state to
|
||||||
// be overriden.
|
// be overriden.
|
||||||
stateOutPath string
|
//
|
||||||
|
|
||||||
// backupPath is used to backup the state file before writing a modified
|
// backupPath is used to backup the state file before writing a modified
|
||||||
// version. It defaults to stateOutPath + DefaultBackupExtention
|
// version. It defaults to stateOutPath + DefaultBackupExtention
|
||||||
backupPath string
|
statePath string
|
||||||
|
stateOutPath string
|
||||||
|
backupPath string
|
||||||
}
|
}
|
||||||
|
|
||||||
// initStatePaths is used to initialize the default values for
|
// 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
|
// StateOutPath returns the true output path for the state file
|
||||||
func (m *Meta) StateOutPath() string {
|
func (m *Meta) StateOutPath() string {
|
||||||
m.initStatePaths()
|
|
||||||
if m.useRemoteState {
|
|
||||||
path, _ := remote.HiddenStatePath()
|
|
||||||
return path
|
|
||||||
}
|
|
||||||
return m.stateOutPath
|
return m.stateOutPath
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -106,14 +99,16 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
||||||
plan, err := terraform.ReadPlan(f)
|
plan, err := terraform.ReadPlan(f)
|
||||||
f.Close()
|
f.Close()
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Check if remote state is enabled, but do not refresh.
|
// Setup our state
|
||||||
// Since a plan is supposed to lock-in the changes, we do not
|
state, statePath, err := StateFromPlan(m.statePath, plan)
|
||||||
// attempt a state refresh.
|
if err != nil {
|
||||||
if plan != nil && plan.State != nil && plan.State.Remote != nil && plan.State.Remote.Type != "" {
|
return nil, false, fmt.Errorf("Error loading plan: %s", err)
|
||||||
log.Printf("[INFO] Enabling remote state from plan")
|
|
||||||
m.useRemoteState = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set our state
|
||||||
|
m.state = state
|
||||||
|
m.stateOutPath = statePath
|
||||||
|
|
||||||
if len(m.variables) > 0 {
|
if len(m.variables) > 0 {
|
||||||
return nil, false, fmt.Errorf(
|
return nil, false, fmt.Errorf(
|
||||||
"You can't set variables with the '-var' or '-var-file' flag\n" +
|
"You can't set variables with the '-var' or '-var-file' flag\n" +
|
||||||
|
@ -132,11 +127,10 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store the loaded state
|
// Store the loaded state
|
||||||
state, err := m.loadState()
|
state, err := m.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, false, err
|
return nil, false, err
|
||||||
}
|
}
|
||||||
m.state = state
|
|
||||||
|
|
||||||
// Load the root module
|
// Load the root module
|
||||||
mod, err := module.NewTreeModule("", copts.Path)
|
mod, err := module.NewTreeModule("", copts.Path)
|
||||||
|
@ -154,7 +148,7 @@ func (m *Meta) Context(copts contextOpts) (*terraform.Context, bool, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
opts.Module = mod
|
opts.Module = mod
|
||||||
opts.State = state
|
opts.State = state.State()
|
||||||
ctx := terraform.NewContext(opts)
|
ctx := terraform.NewContext(opts)
|
||||||
return ctx, false, nil
|
return ctx, false, nil
|
||||||
}
|
}
|
||||||
|
@ -175,6 +169,53 @@ func (m *Meta) InputMode() terraform.InputMode {
|
||||||
return mode
|
return mode
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// State returns the state for this meta.
|
||||||
|
func (m *Meta) State() (state.State, error) {
|
||||||
|
if m.state != nil {
|
||||||
|
return m.state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := State(m.StateOpts())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.state = result.State
|
||||||
|
m.stateOutPath = result.StatePath
|
||||||
|
m.stateResult = result
|
||||||
|
return m.state, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateRaw is used to setup the state manually.
|
||||||
|
func (m *Meta) StateRaw(opts *StateOpts) (*StateResult, error) {
|
||||||
|
result, err := State(opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.state = result.State
|
||||||
|
m.stateOutPath = result.StatePath
|
||||||
|
m.stateResult = result
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// StateOpts returns the default state options
|
||||||
|
func (m *Meta) StateOpts() *StateOpts {
|
||||||
|
localPath := m.statePath
|
||||||
|
if localPath == "" {
|
||||||
|
localPath = DefaultStateFilename
|
||||||
|
}
|
||||||
|
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
|
||||||
|
|
||||||
|
return &StateOpts{
|
||||||
|
LocalPath: localPath,
|
||||||
|
LocalPathOut: m.stateOutPath,
|
||||||
|
RemotePath: remotePath,
|
||||||
|
RemoteRefresh: true,
|
||||||
|
BackupPath: m.backupPath,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// UIInput returns a UIInput object to be used for asking for input.
|
// UIInput returns a UIInput object to be used for asking for input.
|
||||||
func (m *Meta) UIInput() terraform.UIInput {
|
func (m *Meta) UIInput() terraform.UIInput {
|
||||||
return &UIInput{
|
return &UIInput{
|
||||||
|
@ -182,115 +223,14 @@ func (m *Meta) UIInput() terraform.UIInput {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// laodState is used to load the Terraform state. We give precedence
|
|
||||||
// to a remote state if enabled, and then check the normal state path.
|
|
||||||
func (m *Meta) loadState() (*terraform.State, error) {
|
|
||||||
// Check if we remote state is enabled
|
|
||||||
localCache, _, err := remote.ReadLocalState()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error loading state: %s", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set the state if enabled
|
|
||||||
var state *terraform.State
|
|
||||||
if localCache != nil {
|
|
||||||
// Refresh the state
|
|
||||||
log.Printf("[INFO] Refreshing local state...")
|
|
||||||
changes, err := remote.RefreshState(localCache.Remote)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Failed to refresh state: %v", err)
|
|
||||||
}
|
|
||||||
switch changes {
|
|
||||||
case remote.StateChangeNoop:
|
|
||||||
case remote.StateChangeInit:
|
|
||||||
case remote.StateChangeLocalNewer:
|
|
||||||
case remote.StateChangeUpdateLocal:
|
|
||||||
// Reload the state since we've udpated
|
|
||||||
localCache, _, err = remote.ReadLocalState()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error loading state: %s", err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%s", changes)
|
|
||||||
}
|
|
||||||
|
|
||||||
state = localCache
|
|
||||||
m.useRemoteState = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load up the state
|
|
||||||
if m.statePath != "" {
|
|
||||||
f, err := os.Open(m.statePath)
|
|
||||||
if err != nil && os.IsNotExist(err) {
|
|
||||||
// If the state file doesn't exist, it is okay, since it
|
|
||||||
// is probably a new infrastructure.
|
|
||||||
err = nil
|
|
||||||
} else if m.useRemoteState && err == nil {
|
|
||||||
err = fmt.Errorf("Remote state enabled, but state file '%s' also present.", m.statePath)
|
|
||||||
f.Close()
|
|
||||||
} else if err == nil {
|
|
||||||
state, err = terraform.ReadState(f)
|
|
||||||
f.Close()
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("Error loading state: %s", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return state, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PersistState is used to write out the state, handling backup of
|
// PersistState is used to write out the state, handling backup of
|
||||||
// the existing state file and respecting path configurations.
|
// the existing state file and respecting path configurations.
|
||||||
func (m *Meta) PersistState(s *terraform.State) error {
|
func (m *Meta) PersistState(s *terraform.State) error {
|
||||||
if m.useRemoteState {
|
if err := m.state.WriteState(s); err != nil {
|
||||||
return m.persistRemoteState(s)
|
|
||||||
}
|
|
||||||
return m.persistLocalState(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
// persistRemoteState is used to handle persisting a state file
|
|
||||||
// when remote state management is enabled
|
|
||||||
func (m *Meta) persistRemoteState(s *terraform.State) error {
|
|
||||||
log.Printf("[INFO] Persisting state to local cache")
|
|
||||||
if err := remote.PersistState(s); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
log.Printf("[INFO] Uploading state to remote store")
|
|
||||||
change, err := remote.PushState(s.Remote, false)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !change.SuccessfulPush() {
|
|
||||||
return fmt.Errorf("Failed to upload state: %s", change)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// persistLocalState is used to handle persisting a state file
|
return m.state.PersistState()
|
||||||
// when remote state management is disabled.
|
|
||||||
func (m *Meta) persistLocalState(s *terraform.State) error {
|
|
||||||
m.initStatePaths()
|
|
||||||
|
|
||||||
// Create a backup of the state before updating
|
|
||||||
if m.backupPath != "-" {
|
|
||||||
log.Printf("[INFO] Writing backup state to: %s", m.backupPath)
|
|
||||||
if err := remote.CopyFile(m.statePath, m.backupPath); err != nil {
|
|
||||||
return fmt.Errorf("Failed to backup state: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Open the new state file
|
|
||||||
fh, err := os.Create(m.stateOutPath)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("Failed to open state file: %v", err)
|
|
||||||
}
|
|
||||||
defer fh.Close()
|
|
||||||
|
|
||||||
// Write out the state
|
|
||||||
if err := terraform.WriteState(s, fh); err != nil {
|
|
||||||
return fmt.Errorf("Failed to encode the state: %v", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Input returns true if we should ask for input for context.
|
// Input returns true if we should ask for input for context.
|
||||||
|
@ -329,11 +269,11 @@ func (m *Meta) contextOpts() *terraform.ContextOpts {
|
||||||
func (m *Meta) flagSet(n string) *flag.FlagSet {
|
func (m *Meta) flagSet(n string) *flag.FlagSet {
|
||||||
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
f := flag.NewFlagSet(n, flag.ContinueOnError)
|
||||||
f.BoolVar(&m.input, "input", true, "input")
|
f.BoolVar(&m.input, "input", true, "input")
|
||||||
f.Var((*FlagVar)(&m.variables), "var", "variables")
|
f.Var((*FlagKV)(&m.variables), "var", "variables")
|
||||||
f.Var((*FlagVarFile)(&m.variables), "var-file", "variable file")
|
f.Var((*FlagKVFile)(&m.variables), "var-file", "variable file")
|
||||||
|
|
||||||
if m.autoKey != "" {
|
if m.autoKey != "" {
|
||||||
f.Var((*FlagVarFile)(&m.autoVariables), m.autoKey, "variable file")
|
f.Var((*FlagKVFile)(&m.autoVariables), m.autoKey, "variable file")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create an io.Writer that writes to our Ui properly for errors.
|
// Create an io.Writer that writes to our Ui properly for errors.
|
||||||
|
|
|
@ -7,7 +7,6 @@ import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -182,156 +181,3 @@ func TestMeta_initStatePaths(t *testing.T) {
|
||||||
t.Fatalf("bad: %#v", m)
|
t.Fatalf("bad: %#v", m)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMeta_persistLocal(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
m := new(Meta)
|
|
||||||
s := terraform.NewState()
|
|
||||||
if err := m.persistLocalState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exists, err := remote.ExistsFile(m.stateOutPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
t.Fatalf("state should exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write again, shoudl backup
|
|
||||||
if err := m.persistLocalState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
exists, err = remote.ExistsFile(m.backupPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
t.Fatalf("backup should exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeta_persistRemote(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
err := remote.EnsureDirectory()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := terraform.NewState()
|
|
||||||
conf, srv := testRemoteState(t, s, 200)
|
|
||||||
s.Remote = conf
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
m := new(Meta)
|
|
||||||
if err := m.persistRemoteState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
local, _, err := remote.ReadLocalState()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if local == nil {
|
|
||||||
t.Fatalf("state should exist")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.persistRemoteState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
backup := remote.LocalDirectory + "/" + remote.BackupHiddenStateFile
|
|
||||||
exists, err := remote.ExistsFile(backup)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
t.Fatalf("backup should exist")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeta_loadState_remote(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
err := remote.EnsureDirectory()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 1000
|
|
||||||
conf, srv := testRemoteState(t, s, 200)
|
|
||||||
s.Remote = conf
|
|
||||||
defer srv.Close()
|
|
||||||
|
|
||||||
s.Serial = 500
|
|
||||||
if err := remote.PersistState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := new(Meta)
|
|
||||||
s1, err := m.loadState()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if s1.Serial < 1000 {
|
|
||||||
t.Fatalf("Bad: %#v", s1)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !m.useRemoteState {
|
|
||||||
t.Fatalf("should enable remote")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeta_loadState_statePath(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
m := new(Meta)
|
|
||||||
|
|
||||||
s := terraform.NewState()
|
|
||||||
s.Serial = 1000
|
|
||||||
if err := m.persistLocalState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s1, err := m.loadState()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if s1.Serial < 1000 {
|
|
||||||
t.Fatalf("Bad: %#v", s1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMeta_loadState_conflict(t *testing.T) {
|
|
||||||
tmp, cwd := testCwd(t)
|
|
||||||
defer testFixCwd(t, tmp, cwd)
|
|
||||||
|
|
||||||
err := remote.EnsureDirectory()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
m := new(Meta)
|
|
||||||
|
|
||||||
s := terraform.NewState()
|
|
||||||
if err := remote.PersistState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if err := m.persistLocalState(s); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = m.loadState()
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("should error with conflict")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -32,12 +32,13 @@ func (c *OutputCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
name := args[0]
|
name := args[0]
|
||||||
|
|
||||||
state, err := c.Meta.loadState()
|
stateStore, err := c.Meta.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state := stateStore.State()
|
||||||
if len(state.RootModule().Outputs) == 0 {
|
if len(state.RootModule().Outputs) == 0 {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"The state file has no outputs defined. Define an output\n" +
|
"The state file has no outputs defined. Define an output\n" +
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"github.com/hashicorp/terraform/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PullCommand struct {
|
type PullCommand struct {
|
||||||
|
@ -20,32 +20,50 @@ func (c *PullCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recover the local state if any
|
// Read out our state
|
||||||
local, _, err := remote.ReadLocalState()
|
s, err := c.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if local == nil || local.Remote == nil {
|
localState := s.State()
|
||||||
|
|
||||||
|
// If remote state isn't enabled, it is a problem.
|
||||||
|
if !localState.IsRemote() {
|
||||||
c.Ui.Error("Remote state not enabled!")
|
c.Ui.Error("Remote state not enabled!")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt the state refresh
|
// We need the CacheState structure in order to do anything
|
||||||
change, err := remote.RefreshState(local.Remote)
|
var cache *state.CacheState
|
||||||
if err != nil {
|
if bs, ok := s.(*state.BackupState); ok {
|
||||||
|
if cs, ok := bs.Real.(*state.CacheState); ok {
|
||||||
|
cache = cs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cache == nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Failed to refresh from remote state: %v", err))
|
"Failed to extract internal CacheState from remote state.\n" +
|
||||||
|
"This is an internal error, please report it as a bug."))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Refresh the state
|
||||||
|
if err := cache.RefreshState(); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"Failed to refresh from remote state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use an error exit code if the update was not a success
|
// Use an error exit code if the update was not a success
|
||||||
|
change := cache.RefreshResult()
|
||||||
if !change.SuccessfulPull() {
|
if !change.SuccessfulPull() {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", change))
|
c.Ui.Error(fmt.Sprintf("%s", change))
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
c.Ui.Output(fmt.Sprintf("%s", change))
|
c.Ui.Output(fmt.Sprintf("%s", change))
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,9 +7,10 @@ import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -46,10 +47,19 @@ func TestPull_local(t *testing.T) {
|
||||||
defer srv.Close()
|
defer srv.Close()
|
||||||
|
|
||||||
// Store the local state
|
// Store the local state
|
||||||
buf := bytes.NewBuffer(nil)
|
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
terraform.WriteState(s, buf)
|
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
|
||||||
remote.EnsureDirectory()
|
t.Fatalf("err: %s", err)
|
||||||
remote.Persist(buf)
|
}
|
||||||
|
f, err := os.Create(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
err = terraform.WriteState(s, f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &PullCommand{
|
c := &PullCommand{
|
||||||
|
|
|
@ -5,7 +5,7 @@ import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"github.com/hashicorp/terraform/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PushCommand struct {
|
type PushCommand struct {
|
||||||
|
@ -22,31 +22,52 @@ func (c *PushCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a remote state file
|
// Read out our state
|
||||||
local, _, err := remote.ReadLocalState()
|
s, err := c.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
c.Ui.Error(fmt.Sprintf("Failed to read state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if local == nil || local.Remote == nil {
|
localState := s.State()
|
||||||
|
|
||||||
|
// If remote state isn't enabled, it is a problem.
|
||||||
|
if !localState.IsRemote() {
|
||||||
c.Ui.Error("Remote state not enabled!")
|
c.Ui.Error("Remote state not enabled!")
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to push the state
|
// We need the CacheState structure in order to do anything
|
||||||
change, err := remote.PushState(local.Remote, force)
|
var cache *state.CacheState
|
||||||
if err != nil {
|
if bs, ok := s.(*state.BackupState); ok {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to push state: %v", err))
|
if cs, ok := bs.Real.(*state.CacheState); ok {
|
||||||
|
cache = cs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if cache == nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"Failed to extract internal CacheState from remote state.\n" +
|
||||||
|
"This is an internal error, please report it as a bug."))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use an error exit code if the update was not a success
|
// Refresh the cache state
|
||||||
if !change.SuccessfulPush() {
|
if err := cache.Cache.RefreshState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", change))
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"Failed to refresh from remote state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
} else {
|
|
||||||
c.Ui.Output(fmt.Sprintf("%s", change))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Write it to the real storage
|
||||||
|
remote := cache.Durable
|
||||||
|
if err := remote.WriteState(cache.Cache.State()); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error writing state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := remote.PersistState(); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error saving state: %s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -41,10 +41,19 @@ func TestPush_local(t *testing.T) {
|
||||||
s.Remote = conf
|
s.Remote = conf
|
||||||
|
|
||||||
// Store the local state
|
// Store the local state
|
||||||
buf := bytes.NewBuffer(nil)
|
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
terraform.WriteState(s, buf)
|
if err := os.MkdirAll(filepath.Dir(statePath), 0755); err != nil {
|
||||||
remote.EnsureDirectory()
|
t.Fatalf("err: %s", err)
|
||||||
remote.Persist(buf)
|
}
|
||||||
|
f, err := os.Create(statePath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
err = terraform.WriteState(s, f)
|
||||||
|
f.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
c := &PushCommand{
|
c := &PushCommand{
|
||||||
|
|
|
@ -5,8 +5,6 @@ import (
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// RefreshCommand is a cli.Command implementation that refreshes the state
|
// RefreshCommand is a cli.Command implementation that refreshes the state
|
||||||
|
@ -44,16 +42,16 @@ func (c *RefreshCommand) Run(args []string) int {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if remote state is enabled
|
// Check if remote state is enabled
|
||||||
remoteEnabled, err := remote.HaveLocalState()
|
state, err := c.State()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to check for remote state: %v", err))
|
c.Ui.Error(fmt.Sprintf("Failed to load state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify that the state path exists. The "ContextArg" function below
|
// Verify that the state path exists. The "ContextArg" function below
|
||||||
// will actually do this, but we want to provide a richer error message
|
// will actually do this, but we want to provide a richer error message
|
||||||
// if possible.
|
// if possible.
|
||||||
if !remoteEnabled {
|
if !state.State().IsRemote() {
|
||||||
if _, err := os.Stat(c.Meta.statePath); err != nil {
|
if _, err := os.Stat(c.Meta.statePath); err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
@ -95,14 +93,14 @@ func (c *RefreshCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
state, err := ctx.Refresh()
|
newState, err := ctx.Refresh()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error refreshing state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
log.Printf("[INFO] Writing state output to: %s", c.Meta.StateOutPath())
|
||||||
if err := c.Meta.PersistState(state); err != nil {
|
if err := c.Meta.PersistState(newState); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
package command
|
package command
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"github.com/hashicorp/terraform/state"
|
||||||
|
"github.com/hashicorp/terraform/state/remote"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -32,81 +31,81 @@ type RemoteCommand struct {
|
||||||
|
|
||||||
func (c *RemoteCommand) Run(args []string) int {
|
func (c *RemoteCommand) Run(args []string) int {
|
||||||
args = c.Meta.process(args, false)
|
args = c.Meta.process(args, false)
|
||||||
var address, accessToken, name, path string
|
config := make(map[string]string)
|
||||||
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
|
cmdFlags := flag.NewFlagSet("remote", flag.ContinueOnError)
|
||||||
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
|
cmdFlags.BoolVar(&c.conf.disableRemote, "disable", false, "")
|
||||||
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
|
cmdFlags.BoolVar(&c.conf.pullOnDisable, "pull", true, "")
|
||||||
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
|
cmdFlags.StringVar(&c.conf.statePath, "state", DefaultStateFilename, "path")
|
||||||
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
|
cmdFlags.StringVar(&c.conf.backupPath, "backup", "", "path")
|
||||||
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
|
cmdFlags.StringVar(&c.remoteConf.Type, "backend", "atlas", "")
|
||||||
cmdFlags.StringVar(&address, "address", "", "")
|
cmdFlags.Var((*FlagKV)(&config), "backend-config", "config")
|
||||||
cmdFlags.StringVar(&accessToken, "access-token", "", "")
|
|
||||||
cmdFlags.StringVar(&name, "name", "", "")
|
|
||||||
cmdFlags.StringVar(&path, "path", "", "")
|
|
||||||
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
cmdFlags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||||
if err := cmdFlags.Parse(args); err != nil {
|
if err := cmdFlags.Parse(args); err != nil {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Show help if given no inputs
|
// Show help if given no inputs
|
||||||
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" &&
|
if !c.conf.disableRemote && c.remoteConf.Type == "atlas" && len(config) == 0 {
|
||||||
name == "" && accessToken == "" {
|
|
||||||
cmdFlags.Usage()
|
cmdFlags.Usage()
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the local state path
|
||||||
|
c.statePath = c.conf.statePath
|
||||||
|
|
||||||
// Populate the various configurations
|
// Populate the various configurations
|
||||||
c.remoteConf.Config = map[string]string{
|
c.remoteConf.Config = config
|
||||||
"address": address,
|
|
||||||
"access_token": accessToken,
|
|
||||||
"name": name,
|
|
||||||
"path": path,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if have an existing local state file
|
// Get the state information. We specifically request the cache only
|
||||||
haveLocal, err := remote.HaveLocalState()
|
// for the remote state here because it is possible the remote state
|
||||||
if err != nil {
|
// is invalid and we don't want to error.
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to check for local state: %v", err))
|
stateOpts := c.StateOpts()
|
||||||
|
stateOpts.RemoteCacheOnly = true
|
||||||
|
if _, err := c.StateRaw(stateOpts); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Error loading local state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have the non-managed state file
|
// Get the local and remote [cached] state
|
||||||
haveNonManaged, err := remote.ExistsFile(c.conf.statePath)
|
localState := c.stateResult.Local.State()
|
||||||
if err != nil {
|
var remoteState *terraform.State
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to check for state file: %v", err))
|
if remote := c.stateResult.Remote; remote != nil {
|
||||||
return 1
|
remoteState = remote.State()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if remote state is being disabled
|
// Check if remote state is being disabled
|
||||||
if c.conf.disableRemote {
|
if c.conf.disableRemote {
|
||||||
if !haveLocal {
|
if !remoteState.IsRemote() {
|
||||||
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
|
c.Ui.Error(fmt.Sprintf("Remote state management not enabled! Aborting."))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if haveNonManaged {
|
if !localState.Empty() {
|
||||||
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
|
c.Ui.Error(fmt.Sprintf("State file already exists at '%s'. Aborting.",
|
||||||
c.conf.statePath))
|
c.conf.statePath))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return c.disableRemoteState()
|
return c.disableRemoteState()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure there is no conflict
|
// Ensure there is no conflict
|
||||||
|
haveCache := !remoteState.Empty()
|
||||||
|
haveLocal := !localState.Empty()
|
||||||
switch {
|
switch {
|
||||||
case haveLocal && haveNonManaged:
|
case haveCache && haveLocal:
|
||||||
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
|
c.Ui.Error(fmt.Sprintf("Remote state is enabled, but non-managed state file '%s' is also present!",
|
||||||
c.conf.statePath))
|
c.conf.statePath))
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
case !haveLocal && !haveNonManaged:
|
case !haveCache && !haveLocal:
|
||||||
// If we don't have either state file, initialize a blank state file
|
// If we don't have either state file, initialize a blank state file
|
||||||
return c.initBlankState()
|
return c.initBlankState()
|
||||||
|
|
||||||
case haveLocal && !haveNonManaged:
|
case haveCache && !haveLocal:
|
||||||
// Update the remote state target potentially
|
// Update the remote state target potentially
|
||||||
return c.updateRemoteConfig()
|
return c.updateRemoteConfig()
|
||||||
|
|
||||||
case !haveLocal && haveNonManaged:
|
case !haveCache && haveLocal:
|
||||||
// Enable remote state management
|
// Enable remote state management
|
||||||
return c.enableRemoteState()
|
return c.enableRemoteState()
|
||||||
}
|
}
|
||||||
|
@ -117,71 +116,66 @@ func (c *RemoteCommand) Run(args []string) int {
|
||||||
// disableRemoteState is used to disable remote state management,
|
// disableRemoteState is used to disable remote state management,
|
||||||
// and move the state file into place.
|
// and move the state file into place.
|
||||||
func (c *RemoteCommand) disableRemoteState() int {
|
func (c *RemoteCommand) disableRemoteState() int {
|
||||||
// Get the local state
|
if c.stateResult == nil {
|
||||||
local, _, err := remote.ReadLocalState()
|
c.Ui.Error(fmt.Sprintf(
|
||||||
if err != nil {
|
"Internal error. State() must be called internally before remote\n" +
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
|
"state can be disabled. Please report this as a bug."))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
if !c.stateResult.State.State().IsRemote() {
|
||||||
|
c.Ui.Error(fmt.Sprintf(
|
||||||
|
"Remote state is not enabled. Can't disable remote state."))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
local := c.stateResult.Local
|
||||||
|
remote := c.stateResult.Remote
|
||||||
|
|
||||||
// Ensure we have the latest state before disabling
|
// Ensure we have the latest state before disabling
|
||||||
if c.conf.pullOnDisable {
|
if c.conf.pullOnDisable {
|
||||||
log.Printf("[INFO] Refreshing local state from remote server")
|
log.Printf("[INFO] Refreshing local state from remote server")
|
||||||
change, err := remote.RefreshState(local.Remote)
|
if err := remote.RefreshState(); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Failed to refresh from remote state: %v", err))
|
"Failed to refresh from remote state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exit if we were unable to update
|
// Exit if we were unable to update
|
||||||
if !change.SuccessfulPull() {
|
if change := remote.RefreshResult(); !change.SuccessfulPull() {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", change))
|
c.Ui.Error(fmt.Sprintf("%s", change))
|
||||||
return 1
|
return 1
|
||||||
} else {
|
} else {
|
||||||
log.Printf("[INFO] %s", change)
|
log.Printf("[INFO] %s", change)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload the local state after the refresh
|
|
||||||
local, _, err = remote.ReadLocalState()
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear the remote management, and copy into place
|
// Clear the remote management, and copy into place
|
||||||
local.Remote = nil
|
newState := remote.State()
|
||||||
fh, err := os.Create(c.conf.statePath)
|
newState.Remote = nil
|
||||||
if err != nil {
|
if err := local.WriteState(newState); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to create state file '%s': %v",
|
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
|
||||||
c.conf.statePath, err))
|
c.conf.statePath, err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
defer fh.Close()
|
if err := local.PersistState(); err != nil {
|
||||||
if err := terraform.WriteState(local, fh); err != nil {
|
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %s",
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to encode state file '%s': %v",
|
|
||||||
c.conf.statePath, err))
|
c.conf.statePath, err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the old state file
|
// Remove the old state file
|
||||||
path, err := remote.HiddenStatePath()
|
if err := os.Remove(c.stateResult.RemotePath); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to get local state path: %v", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
if err := os.Remove(path); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
|
c.Ui.Error(fmt.Sprintf("Failed to remove the local state file: %v", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// validateRemoteConfig is used to verify that the remote configuration
|
// validateRemoteConfig is used to verify that the remote configuration
|
||||||
// we have is valid
|
// we have is valid
|
||||||
func (c *RemoteCommand) validateRemoteConfig() error {
|
func (c *RemoteCommand) validateRemoteConfig() error {
|
||||||
err := remote.ValidConfig(&c.remoteConf)
|
conf := c.remoteConf
|
||||||
|
_, err := remote.NewClient(conf.Type, conf.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||||
}
|
}
|
||||||
|
@ -196,18 +190,17 @@ func (c *RemoteCommand) initBlankState() int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the hidden directory
|
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make a blank state, attach the remote configuration
|
// Make a blank state, attach the remote configuration
|
||||||
blank := terraform.NewState()
|
blank := terraform.NewState()
|
||||||
blank.Remote = &c.remoteConf
|
blank.Remote = &c.remoteConf
|
||||||
|
|
||||||
// Persist the state
|
// Persist the state
|
||||||
if err := remote.PersistState(blank); err != nil {
|
remote := &state.LocalState{Path: c.stateResult.RemotePath}
|
||||||
|
if err := remote.WriteState(blank); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := remote.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
|
c.Ui.Error(fmt.Sprintf("Failed to initialize state file: %v", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
@ -225,16 +218,17 @@ func (c *RemoteCommand) updateRemoteConfig() int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read in the local state
|
// Read in the local state, which is just the cache of the remote state
|
||||||
local, _, err := remote.ReadLocalState()
|
remote := c.stateResult.Remote.Cache
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to read local state: %v", err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update the configuration
|
// Update the configuration
|
||||||
local.Remote = &c.remoteConf
|
state := remote.State()
|
||||||
if err := remote.PersistState(local); err != nil {
|
state.Remote = &c.remoteConf
|
||||||
|
if err := remote.WriteState(state); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := remote.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
@ -252,21 +246,10 @@ func (c *RemoteCommand) enableRemoteState() int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the hidden directory
|
// Read the local state
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
local := c.stateResult.Local
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
if err := local.RefreshState(); err != nil {
|
||||||
return 1
|
c.Ui.Error(fmt.Sprintf("Failed to read local state: %s", err))
|
||||||
}
|
|
||||||
|
|
||||||
// Read the provided state file
|
|
||||||
raw, err := ioutil.ReadFile(c.conf.statePath)
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to read '%s': %v", c.conf.statePath, err))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
state, err := terraform.ReadState(bytes.NewReader(raw))
|
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to decode '%s': %v", c.conf.statePath, err))
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -279,25 +262,31 @@ func (c *RemoteCommand) enableRemoteState() int {
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("[INFO] Writing backup state to: %s", backupPath)
|
log.Printf("[INFO] Writing backup state to: %s", backupPath)
|
||||||
f, err := os.Create(backupPath)
|
backup := &state.LocalState{Path: backupPath}
|
||||||
if err == nil {
|
if err := backup.WriteState(local.State()); err != nil {
|
||||||
err = terraform.WriteState(state, f)
|
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
|
||||||
f.Close()
|
return 1
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err := backup.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error writing backup state file: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update the local configuration, move into place
|
// Update the local configuration, move into place
|
||||||
|
state := local.State()
|
||||||
state.Remote = &c.remoteConf
|
state.Remote = &c.remoteConf
|
||||||
if err := remote.PersistState(state); err != nil {
|
remote := c.stateResult.Remote
|
||||||
|
if err := remote.WriteState(state); err != nil {
|
||||||
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if err := remote.PersistState(); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("%s", err))
|
c.Ui.Error(fmt.Sprintf("%s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove the state file
|
// Remove the original, local state file
|
||||||
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
|
log.Printf("[INFO] Removing state file: %s", c.conf.statePath)
|
||||||
if err := os.Remove(c.conf.statePath); err != nil {
|
if err := os.Remove(c.conf.statePath); err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
|
c.Ui.Error(fmt.Sprintf("Failed to remove state file '%s': %v",
|
||||||
|
@ -321,15 +310,12 @@ Usage: terraform remote [options]
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
|
|
||||||
-address=url URL of the remote storage server.
|
|
||||||
Required for HTTP backend, optional for Atlas and Consul.
|
|
||||||
|
|
||||||
-access-token=token Authentication token for state storage server.
|
|
||||||
Required for Atlas backend, optional for Consul.
|
|
||||||
|
|
||||||
-backend=Atlas Specifies the type of remote backend. Must be one
|
-backend=Atlas Specifies the type of remote backend. Must be one
|
||||||
of Atlas, Consul, or HTTP. Defaults to Atlas.
|
of Atlas, Consul, or HTTP. Defaults to Atlas.
|
||||||
|
|
||||||
|
-backend-config="k=v" Specifies configuration for the remote storage
|
||||||
|
backend. This can be specified multiple times.
|
||||||
|
|
||||||
-backup=path Path to backup the existing state file before
|
-backup=path Path to backup the existing state file before
|
||||||
modifying. Defaults to the "-state" path with
|
modifying. Defaults to the "-state" path with
|
||||||
".backup" extension. Set to "-" to disable backup.
|
".backup" extension. Set to "-" to disable backup.
|
||||||
|
@ -337,15 +323,9 @@ Options:
|
||||||
-disable Disables remote state management and migrates the state
|
-disable Disables remote state management and migrates the state
|
||||||
to the -state path.
|
to the -state path.
|
||||||
|
|
||||||
-name=name Name of the state file in the state storage server.
|
|
||||||
Required for Atlas backend.
|
|
||||||
|
|
||||||
-path=path Path of the remote state in Consul. Required for the
|
|
||||||
Consul backend.
|
|
||||||
|
|
||||||
-pull=true Controls if the remote state is pulled before disabling.
|
-pull=true Controls if the remote state is pulled before disabling.
|
||||||
This defaults to true to ensure the latest state is cached
|
This defaults to true to ensure the latest state is cached
|
||||||
before disabling.
|
before disabling.
|
||||||
|
|
||||||
-state=path Path to read state. Defaults to "terraform.tfstate"
|
-state=path Path to read state. Defaults to "terraform.tfstate"
|
||||||
unless remote state is enabled.
|
unless remote state is enabled.
|
||||||
|
|
|
@ -4,9 +4,10 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hashicorp/terraform/remote"
|
"github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
"github.com/mitchellh/cli"
|
"github.com/mitchellh/cli"
|
||||||
)
|
)
|
||||||
|
@ -26,11 +27,15 @@ func TestRemote_disable(t *testing.T) {
|
||||||
s = terraform.NewState()
|
s = terraform.NewState()
|
||||||
s.Serial = 5
|
s.Serial = 5
|
||||||
s.Remote = conf
|
s.Remote = conf
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
// Write the state
|
||||||
|
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
|
state := &state.LocalState{Path: statePath}
|
||||||
|
if err := state.WriteState(s); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if err := remote.PersistState(s); err != nil {
|
if err := state.PersistState(); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
@ -45,23 +50,9 @@ func TestRemote_disable(t *testing.T) {
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local state file should be removed
|
// Local state file should be removed and the local cache should exist
|
||||||
haveLocal, err := remote.HaveLocalState()
|
testRemoteLocal(t, true)
|
||||||
if err != nil {
|
testRemoteLocalCache(t, false)
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if haveLocal {
|
|
||||||
t.Fatalf("should be disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// New state file should be installed
|
|
||||||
exists, err := remote.ExistsFile(DefaultStateFilename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
t.Fatalf("failed to make state file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the state file was updated
|
// Check that the state file was updated
|
||||||
raw, _ := ioutil.ReadFile(DefaultStateFilename)
|
raw, _ := ioutil.ReadFile(DefaultStateFilename)
|
||||||
|
@ -71,11 +62,6 @@ func TestRemote_disable(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we updated
|
// Ensure we updated
|
||||||
// TODO: Should be 10, but WriteState currently
|
|
||||||
// increments incorrectly
|
|
||||||
if newState.Serial != 11 {
|
|
||||||
t.Fatalf("state file not updated: %#v", newState)
|
|
||||||
}
|
|
||||||
if newState.Remote != nil {
|
if newState.Remote != nil {
|
||||||
t.Fatalf("remote configuration not removed")
|
t.Fatalf("remote configuration not removed")
|
||||||
}
|
}
|
||||||
|
@ -96,11 +82,15 @@ func TestRemote_disable_noPull(t *testing.T) {
|
||||||
s = terraform.NewState()
|
s = terraform.NewState()
|
||||||
s.Serial = 5
|
s.Serial = 5
|
||||||
s.Remote = conf
|
s.Remote = conf
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
// Write the state
|
||||||
|
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
|
state := &state.LocalState{Path: statePath}
|
||||||
|
if err := state.WriteState(s); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if err := remote.PersistState(s); err != nil {
|
if err := state.PersistState(); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
@ -115,23 +105,9 @@ func TestRemote_disable_noPull(t *testing.T) {
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local state file should be removed
|
// Local state file should be removed and the local cache should exist
|
||||||
haveLocal, err := remote.HaveLocalState()
|
testRemoteLocal(t, true)
|
||||||
if err != nil {
|
testRemoteLocalCache(t, false)
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if haveLocal {
|
|
||||||
t.Fatalf("should be disabled")
|
|
||||||
}
|
|
||||||
|
|
||||||
// New state file should be installed
|
|
||||||
exists, err := remote.ExistsFile(DefaultStateFilename)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
t.Fatalf("failed to make state file")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check that the state file was updated
|
// Check that the state file was updated
|
||||||
raw, _ := ioutil.ReadFile(DefaultStateFilename)
|
raw, _ := ioutil.ReadFile(DefaultStateFilename)
|
||||||
|
@ -140,12 +116,6 @@ func TestRemote_disable_noPull(t *testing.T) {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we DIDNT updated
|
|
||||||
// TODO: Should be 5, but WriteState currently increments
|
|
||||||
// this which is incorrect.
|
|
||||||
if newState.Serial != 7 {
|
|
||||||
t.Fatalf("state file updated: %#v", newState)
|
|
||||||
}
|
|
||||||
if newState.Remote != nil {
|
if newState.Remote != nil {
|
||||||
t.Fatalf("remote configuration not removed")
|
t.Fatalf("remote configuration not removed")
|
||||||
}
|
}
|
||||||
|
@ -178,11 +148,15 @@ func TestRemote_disable_otherState(t *testing.T) {
|
||||||
// Persist local remote state
|
// Persist local remote state
|
||||||
s := terraform.NewState()
|
s := terraform.NewState()
|
||||||
s.Serial = 5
|
s.Serial = 5
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
// Write the state
|
||||||
|
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
|
state := &state.LocalState{Path: statePath}
|
||||||
|
if err := state.WriteState(s); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if err := remote.PersistState(s); err != nil {
|
if err := state.PersistState(); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also put a file at the default path
|
// Also put a file at the default path
|
||||||
|
@ -218,11 +192,15 @@ func TestRemote_managedAndNonManaged(t *testing.T) {
|
||||||
// Persist local remote state
|
// Persist local remote state
|
||||||
s := terraform.NewState()
|
s := terraform.NewState()
|
||||||
s.Serial = 5
|
s.Serial = 5
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
// Write the state
|
||||||
|
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
|
state := &state.LocalState{Path: statePath}
|
||||||
|
if err := state.WriteState(s); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if err := remote.PersistState(s); err != nil {
|
if err := state.PersistState(); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Also put a file at the default path
|
// Also put a file at the default path
|
||||||
|
@ -265,18 +243,20 @@ func TestRemote_initBlank(t *testing.T) {
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-backend=http",
|
"-backend=http",
|
||||||
"-address", "http://example.com",
|
"-backend-config", "address=http://example.com",
|
||||||
"-access-token=test",
|
"-backend-config", "access_token=test",
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
local, _, err := remote.ReadLocalState()
|
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
|
||||||
if err != nil {
|
ls := &state.LocalState{Path: remotePath}
|
||||||
t.Fatalf("err: %v", err)
|
if err := ls.RefreshState(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local := ls.State()
|
||||||
if local.Remote.Type != "http" {
|
if local.Remote.Type != "http" {
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
t.Fatalf("Bad: %#v", local.Remote)
|
||||||
}
|
}
|
||||||
|
@ -318,11 +298,15 @@ func TestRemote_updateRemote(t *testing.T) {
|
||||||
s.Remote = &terraform.RemoteState{
|
s.Remote = &terraform.RemoteState{
|
||||||
Type: "invalid",
|
Type: "invalid",
|
||||||
}
|
}
|
||||||
if err := remote.EnsureDirectory(); err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
// Write the state
|
||||||
|
statePath := filepath.Join(tmp, DefaultDataDir, DefaultStateFilename)
|
||||||
|
ls := &state.LocalState{Path: statePath}
|
||||||
|
if err := ls.WriteState(s); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
if err := remote.PersistState(s); err != nil {
|
if err := ls.PersistState(); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
ui := new(cli.MockUi)
|
ui := new(cli.MockUi)
|
||||||
|
@ -335,18 +319,19 @@ func TestRemote_updateRemote(t *testing.T) {
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-backend=http",
|
"-backend=http",
|
||||||
"-address",
|
"-backend-config", "address=http://example.com",
|
||||||
"http://example.com",
|
"-backend-config", "access_token=test",
|
||||||
"-access-token=test",
|
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
local, _, err := remote.ReadLocalState()
|
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
|
||||||
if err != nil {
|
ls = &state.LocalState{Path: remotePath}
|
||||||
t.Fatalf("err: %v", err)
|
if err := ls.RefreshState(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
local := ls.State()
|
||||||
|
|
||||||
if local.Remote.Type != "http" {
|
if local.Remote.Type != "http" {
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
t.Fatalf("Bad: %#v", local.Remote)
|
||||||
|
@ -389,18 +374,19 @@ func TestRemote_enableRemote(t *testing.T) {
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-backend=http",
|
"-backend=http",
|
||||||
"-address",
|
"-backend-config", "address=http://example.com",
|
||||||
"http://example.com",
|
"-backend-config", "access_token=test",
|
||||||
"-access-token=test",
|
|
||||||
}
|
}
|
||||||
if code := c.Run(args); code != 0 {
|
if code := c.Run(args); code != 0 {
|
||||||
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
t.Fatalf("bad: \n%s", ui.ErrorWriter.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
local, _, err := remote.ReadLocalState()
|
remotePath := filepath.Join(DefaultDataDir, DefaultStateFilename)
|
||||||
if err != nil {
|
ls := &state.LocalState{Path: remotePath}
|
||||||
t.Fatalf("err: %v", err)
|
if err := ls.RefreshState(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
}
|
}
|
||||||
|
local := ls.State()
|
||||||
|
|
||||||
if local.Remote.Type != "http" {
|
if local.Remote.Type != "http" {
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
t.Fatalf("Bad: %#v", local.Remote)
|
||||||
|
@ -412,21 +398,49 @@ func TestRemote_enableRemote(t *testing.T) {
|
||||||
t.Fatalf("Bad: %#v", local.Remote)
|
t.Fatalf("Bad: %#v", local.Remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backup file should exist
|
// Backup file should exist, state file should not
|
||||||
exist, err := remote.ExistsFile(DefaultStateFilename + DefaultBackupExtention)
|
testRemoteLocal(t, false)
|
||||||
if err != nil {
|
testRemoteLocalBackup(t, true)
|
||||||
t.Fatalf("err: %v", err)
|
}
|
||||||
|
|
||||||
|
func testRemoteLocal(t *testing.T, exists bool) {
|
||||||
|
_, err := os.Stat(DefaultStateFilename)
|
||||||
|
if os.IsNotExist(err) && !exists {
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if !exist {
|
if err == nil && exists {
|
||||||
t.Fatalf("backup should exist")
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// State file should not
|
t.Fatalf("bad: %#v", err)
|
||||||
exist, err = remote.ExistsFile(DefaultStateFilename)
|
}
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("err: %v", err)
|
func testRemoteLocalBackup(t *testing.T, exists bool) {
|
||||||
}
|
_, err := os.Stat(DefaultStateFilename + DefaultBackupExtention)
|
||||||
if exist {
|
if os.IsNotExist(err) && !exists {
|
||||||
t.Fatalf("state file should not exist")
|
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"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
statelib "github.com/hashicorp/terraform/state"
|
||||||
"github.com/hashicorp/terraform/terraform"
|
"github.com/hashicorp/terraform/terraform"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,7 +37,7 @@ func (c *ShowCommand) Run(args []string) int {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
var err, planErr, stateErr error
|
var planErr, stateErr error
|
||||||
var path string
|
var path string
|
||||||
var plan *terraform.Plan
|
var plan *terraform.Plan
|
||||||
var state *terraform.State
|
var state *terraform.State
|
||||||
|
@ -68,12 +69,13 @@ func (c *ShowCommand) Run(args []string) int {
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// We should use the default state if it exists.
|
// We should use the default state if it exists.
|
||||||
c.Meta.statePath = DefaultStateFilename
|
stateStore := &statelib.LocalState{Path: DefaultStateFilename}
|
||||||
state, err = c.Meta.loadState()
|
if err := stateStore.RefreshState(); err != nil {
|
||||||
if err != nil {
|
|
||||||
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
|
c.Ui.Error(fmt.Sprintf("Error reading state: %s", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
|
state = stateStore.State()
|
||||||
if state == nil {
|
if state == nil {
|
||||||
c.Ui.Output("No state.")
|
c.Ui.Output("No state.")
|
||||||
return 0
|
return 0
|
||||||
|
|
|
@ -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.
|
// Ui is the cli.Ui used for communicating to the outside world.
|
||||||
var Ui cli.Ui
|
var Ui cli.Ui
|
||||||
|
|
||||||
const ErrorPrefix = "e:"
|
const (
|
||||||
const OutputPrefix = "o:"
|
ErrorPrefix = "e:"
|
||||||
|
OutputPrefix = "o:"
|
||||||
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Ui = &cli.PrefixedUi{
|
Ui = &cli.PrefixedUi{
|
||||||
|
|
|
@ -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/"
|
defaultAtlasServer = "https://atlas.hashicorp.com/"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AtlasRemoteClient implements the RemoteClient interface
|
func atlasFactory(conf map[string]string) (Client, error) {
|
||||||
// for an Atlas compatible server.
|
var client AtlasClient
|
||||||
type AtlasRemoteClient struct {
|
|
||||||
server string
|
|
||||||
serverURL *url.URL
|
|
||||||
user string
|
|
||||||
name string
|
|
||||||
accessToken string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAtlasRemoteClient(conf map[string]string) (*AtlasRemoteClient, error) {
|
|
||||||
client := &AtlasRemoteClient{}
|
|
||||||
if err := client.validateConfig(conf); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return client, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error {
|
|
||||||
server, ok := conf["address"]
|
server, ok := conf["address"]
|
||||||
if !ok || server == "" {
|
if !ok || server == "" {
|
||||||
server = defaultAtlasServer
|
server = defaultAtlasServer
|
||||||
}
|
}
|
||||||
|
|
||||||
url, err := url.Parse(server)
|
url, err := url.Parse(server)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
c.server = server
|
|
||||||
c.serverURL = url
|
|
||||||
|
|
||||||
token, ok := conf["access_token"]
|
token, ok := conf["access_token"]
|
||||||
if token == "" {
|
if token == "" {
|
||||||
|
@ -54,26 +37,39 @@ func (c *AtlasRemoteClient) validateConfig(conf map[string]string) error {
|
||||||
ok = true
|
ok = true
|
||||||
}
|
}
|
||||||
if !ok || token == "" {
|
if !ok || token == "" {
|
||||||
return fmt.Errorf(
|
return nil, fmt.Errorf(
|
||||||
"missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
|
"missing 'access_token' configuration or ATLAS_TOKEN environmental variable")
|
||||||
}
|
}
|
||||||
c.accessToken = token
|
|
||||||
|
|
||||||
name, ok := conf["name"]
|
name, ok := conf["name"]
|
||||||
if !ok || name == "" {
|
if !ok || name == "" {
|
||||||
return fmt.Errorf("missing 'name' configuration")
|
return nil, fmt.Errorf("missing 'name' configuration")
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(name, "/")
|
parts := strings.Split(name, "/")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
return fmt.Errorf("malformed name '%s'", name)
|
return nil, fmt.Errorf("malformed name '%s'", name)
|
||||||
}
|
}
|
||||||
c.user = parts[0]
|
|
||||||
c.name = parts[1]
|
client.Server = server
|
||||||
return nil
|
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
|
// Make the HTTP request
|
||||||
req, err := http.NewRequest("GET", c.url().String(), nil)
|
req, err := http.NewRequest("GET", c.url().String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -96,11 +92,11 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
case http.StatusUnauthorized:
|
case http.StatusUnauthorized:
|
||||||
return nil, ErrRequireAuth
|
return nil, fmt.Errorf("HTTP remote state endpoint requires auth")
|
||||||
case http.StatusForbidden:
|
case http.StatusForbidden:
|
||||||
return nil, ErrInvalidAuth
|
return nil, fmt.Errorf("HTTP remote state endpoint invalid auth")
|
||||||
case http.StatusInternalServerError:
|
case http.StatusInternalServerError:
|
||||||
return nil, ErrRemoteInternal
|
return nil, fmt.Errorf("HTTP remote state internal server error")
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
|
return nil, fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
@ -112,8 +108,12 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create the payload
|
// Create the payload
|
||||||
payload := &RemoteStatePayload{
|
payload := &Payload{
|
||||||
State: buf.Bytes(),
|
Data: buf.Bytes(),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(payload.Data) == 0 {
|
||||||
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for the MD5
|
// Check for the MD5
|
||||||
|
@ -122,18 +122,18 @@ func (c *AtlasRemoteClient) GetState() (*RemoteStatePayload, error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
|
return nil, fmt.Errorf("Failed to decode Content-MD5 '%s': %v", raw, err)
|
||||||
}
|
}
|
||||||
payload.MD5 = md5
|
|
||||||
|
|
||||||
|
payload.MD5 = md5
|
||||||
} else {
|
} else {
|
||||||
// Generate the MD5
|
// Generate the MD5
|
||||||
hash := md5.Sum(payload.State)
|
hash := md5.Sum(payload.Data)
|
||||||
payload.MD5 = hash[:md5.Size]
|
payload.MD5 = hash[:]
|
||||||
}
|
}
|
||||||
|
|
||||||
return payload, nil
|
return payload, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
|
func (c *AtlasClient) Put(state []byte) error {
|
||||||
// Get the target URL
|
// Get the target URL
|
||||||
base := c.url()
|
base := c.url()
|
||||||
|
|
||||||
|
@ -141,12 +141,14 @@ func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
|
||||||
hash := md5.Sum(state)
|
hash := md5.Sum(state)
|
||||||
b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size])
|
b64 := base64.StdEncoding.EncodeToString(hash[:md5.Size])
|
||||||
|
|
||||||
// Set the force query parameter if needed
|
/*
|
||||||
if force {
|
// Set the force query parameter if needed
|
||||||
values := base.Query()
|
if force {
|
||||||
values.Set("force", "true")
|
values := base.Query()
|
||||||
base.RawQuery = values.Encode()
|
values.Set("force", "true")
|
||||||
}
|
base.RawQuery = values.Encode()
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
// Make the HTTP client and request
|
// Make the HTTP client and request
|
||||||
req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state))
|
req, err := http.NewRequest("PUT", base.String(), bytes.NewReader(state))
|
||||||
|
@ -170,22 +172,12 @@ func (c *AtlasRemoteClient) PutState(state []byte, force bool) error {
|
||||||
switch resp.StatusCode {
|
switch resp.StatusCode {
|
||||||
case http.StatusOK:
|
case http.StatusOK:
|
||||||
return nil
|
return nil
|
||||||
case http.StatusConflict:
|
|
||||||
return ErrConflict
|
|
||||||
case http.StatusPreconditionFailed:
|
|
||||||
return ErrServerNewer
|
|
||||||
case http.StatusUnauthorized:
|
|
||||||
return ErrRequireAuth
|
|
||||||
case http.StatusForbidden:
|
|
||||||
return ErrInvalidAuth
|
|
||||||
case http.StatusInternalServerError:
|
|
||||||
return ErrRemoteInternal
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
|
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AtlasRemoteClient) DeleteState() error {
|
func (c *AtlasClient) Delete() error {
|
||||||
// Make the HTTP request
|
// Make the HTTP request
|
||||||
req, err := http.NewRequest("DELETE", c.url().String(), nil)
|
req, err := http.NewRequest("DELETE", c.url().String(), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -207,22 +199,18 @@ func (c *AtlasRemoteClient) DeleteState() error {
|
||||||
return nil
|
return nil
|
||||||
case http.StatusNotFound:
|
case http.StatusNotFound:
|
||||||
return nil
|
return nil
|
||||||
case http.StatusUnauthorized:
|
default:
|
||||||
return ErrRequireAuth
|
return fmt.Errorf("HTTP error: %d", resp.StatusCode)
|
||||||
case http.StatusForbidden:
|
|
||||||
return ErrInvalidAuth
|
|
||||||
case http.StatusInternalServerError:
|
|
||||||
return ErrRemoteInternal
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
|
return fmt.Errorf("Unexpected HTTP response code %d", resp.StatusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *AtlasRemoteClient) url() *url.URL {
|
func (c *AtlasClient) url() *url.URL {
|
||||||
return &url.URL{
|
return &url.URL{
|
||||||
Scheme: c.serverURL.Scheme,
|
Scheme: c.ServerURL.Scheme,
|
||||||
Host: c.serverURL.Host,
|
Host: c.ServerURL.Host,
|
||||||
Path: path.Join("api/v1/terraform/state", c.user, c.name),
|
Path: path.Join("api/v1/terraform/state", c.User, c.Name),
|
||||||
RawQuery: fmt.Sprintf("access_token=%s", c.accessToken),
|
RawQuery: fmt.Sprintf("access_token=%s", c.AccessToken),
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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
|
return orphans
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Empty returns true if the state is empty.
|
||||||
|
func (s *State) Empty() bool {
|
||||||
|
if s == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(s.Modules) == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsRemote returns true if State represents a state that exists and is
|
||||||
|
// remote.
|
||||||
|
func (s *State) IsRemote() bool {
|
||||||
|
if s == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.Remote == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if s.Remote.Type == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// RootModule returns the ModuleState for the root module
|
// RootModule returns the ModuleState for the root module
|
||||||
func (s *State) RootModule() *ModuleState {
|
func (s *State) RootModule() *ModuleState {
|
||||||
root := s.ModuleByPath(rootModulePath)
|
root := s.ModuleByPath(rootModulePath)
|
||||||
|
@ -266,7 +291,7 @@ func (r *RemoteState) deepcopy() *RemoteState {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RemoteState) Empty() bool {
|
func (r *RemoteState) Empty() bool {
|
||||||
return r.Type == "" && len(r.Config) == 0
|
return r == nil || r.Type == ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *RemoteState) Equals(other *RemoteState) bool {
|
func (r *RemoteState) Equals(other *RemoteState) bool {
|
||||||
|
|
|
@ -312,6 +312,70 @@ func TestInstanceStateEqual(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestStateEmpty(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
In *State
|
||||||
|
Result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
nil,
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&State{},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&State{
|
||||||
|
Remote: &RemoteState{Type: "foo"},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&State{
|
||||||
|
Modules: []*ModuleState{
|
||||||
|
&ModuleState{},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
if tc.In.Empty() != tc.Result {
|
||||||
|
t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStateIsRemote(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
In *State
|
||||||
|
Result bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
nil,
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&State{},
|
||||||
|
false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
&State{
|
||||||
|
Remote: &RemoteState{Type: "foo"},
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, tc := range cases {
|
||||||
|
if tc.In.IsRemote() != tc.Result {
|
||||||
|
t.Fatalf("bad %d %#v:\n\n%#v", i, tc.Result, tc.In)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestInstanceState_MergeDiff(t *testing.T) {
|
func TestInstanceState_MergeDiff(t *testing.T) {
|
||||||
is := InstanceState{
|
is := InstanceState{
|
||||||
ID: "foo",
|
ID: "foo",
|
||||||
|
|
Loading…
Reference in New Issue