remote: implement refresh state
This commit is contained in:
parent
d077a82db2
commit
d332b8ad58
|
@ -99,18 +99,18 @@ func (c *InitCommand) Run(args []string) int {
|
||||||
|
|
||||||
// Handle remote state if configured
|
// Handle remote state if configured
|
||||||
if !remoteConf.Empty() {
|
if !remoteConf.Empty() {
|
||||||
// Read the updated state file
|
change, err := remote.RefreshState(&remoteConf)
|
||||||
remoteR, err := remote.ReadState(&remoteConf)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.Ui.Error(fmt.Sprintf(
|
c.Ui.Error(fmt.Sprintf(
|
||||||
"Failed to read remote state: %v", err))
|
"Failed to refresh from remote state: %v", err))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist the remote state
|
// Log the change that took place
|
||||||
if err := remote.Persist(remoteR); err != nil {
|
c.Ui.Output(fmt.Sprintf("%s", change))
|
||||||
c.Ui.Error(fmt.Sprintf(
|
|
||||||
"Failed to persist state: %v", err))
|
// Use an error exit code if the update was not a success
|
||||||
|
if !change.SuccessfulPull() {
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
187
remote/remote.go
187
remote/remote.go
|
@ -2,8 +2,10 @@ package remote
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
@ -29,6 +31,82 @@ const (
|
||||||
DefaultServer = "http://www.hashicorp.com/"
|
DefaultServer = "http://www.hashicorp.com/"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
|
||||||
|
// 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 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 StateChangeUpdateLocal:
|
||||||
|
return true
|
||||||
|
case StateChangeLocalNewer:
|
||||||
|
return false
|
||||||
|
case StateChangeConflict:
|
||||||
|
return false
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// EnsureDirectory is used to make sure the local storage
|
// EnsureDirectory is used to make sure the local storage
|
||||||
// directory exists
|
// directory exists
|
||||||
func EnsureDirectory() error {
|
func EnsureDirectory() error {
|
||||||
|
@ -126,26 +204,113 @@ This is likely a bug, please report it.`)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReadState is used to read the remote state given
|
// RefreshState is used to read the remote state given
|
||||||
// the configuration for the remote endpoint. We return
|
// the configuration for the remote endpoint, and update
|
||||||
// a boolean indicating if the remote state exists, along
|
// the local state if necessary.
|
||||||
// with the state, and possible error.
|
func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) {
|
||||||
func ReadState(conf *terraform.RemoteState) (io.Reader, error) {
|
// Read the state from the server
|
||||||
// TODO: Read actually from a server
|
payload, err := GetState(conf)
|
||||||
|
if err != nil {
|
||||||
|
return StateChangeNoop,
|
||||||
|
fmt.Errorf("Failed to read remote state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Return the blank state, which is done if the server
|
// Parse the remote state
|
||||||
// returns a "not found" or equivalent
|
var remoteState *terraform.State
|
||||||
return blankState(conf)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the path to the state file
|
||||||
|
path, err := HiddenStatePath()
|
||||||
|
if err != nil {
|
||||||
|
return StateChangeNoop, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the existing state file
|
||||||
|
raw, err := ioutil.ReadFile(path)
|
||||||
|
if err != nil && !os.IsNotExist(err) {
|
||||||
|
return StateChangeNoop, fmt.Errorf("Failed to read local state: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode the state
|
||||||
|
var localState *terraform.State
|
||||||
|
if raw != nil {
|
||||||
|
localState, err = terraform.ReadState(bytes.NewReader(raw))
|
||||||
|
if err != nil {
|
||||||
|
return StateChangeNoop,
|
||||||
|
fmt.Errorf("Failed to decode state file '%s': %v", path, 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 StateChangeNoop, nil
|
||||||
|
|
||||||
|
case remoteState == nil && localState != nil:
|
||||||
|
fallthrough
|
||||||
|
case remoteState.Serial < localState.Serial:
|
||||||
|
// User should probably do a push, nothing to do
|
||||||
|
return StateChangeLocalNewer, nil
|
||||||
|
|
||||||
|
case remoteState != nil && localState == nil:
|
||||||
|
fallthrough
|
||||||
|
case remoteState.Serial > localState.Serial:
|
||||||
|
// 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
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should not reach this point
|
||||||
|
panic("Unhandled remote update case")
|
||||||
}
|
}
|
||||||
|
|
||||||
// blankState is used to return a serialized form of a blank state
|
// blankState is used to return a serialized form of a blank state
|
||||||
// with only the remote info.
|
// with only the remote info.
|
||||||
func blankState(conf *terraform.RemoteState) (io.Reader, error) {
|
func blankState(conf *terraform.RemoteState) ([]byte, error) {
|
||||||
blank := terraform.NewState()
|
blank := terraform.NewState()
|
||||||
blank.Remote = conf
|
blank.Remote = conf
|
||||||
buf := bytes.NewBuffer(nil)
|
buf := bytes.NewBuffer(nil)
|
||||||
err := terraform.WriteState(blank, buf)
|
err := terraform.WriteState(blank, buf)
|
||||||
return buf, err
|
return buf.Bytes(), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Persist is used to write out the state given by a reader (likely
|
// Persist is used to write out the state given by a reader (likely
|
||||||
|
|
|
@ -68,7 +68,19 @@ func TestValidateConfig(t *testing.T) {
|
||||||
// TODO:
|
// TODO:
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestReadState(t *testing.T) {
|
func TestRefreshState_Blank(t *testing.T) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshState_Update_Newer(t *testing.T) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshState_Update_Older(t *testing.T) {
|
||||||
|
// TODO
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRefreshState_Noop(t *testing.T) {
|
||||||
// TODO
|
// TODO
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,7 +94,7 @@ func TestBlankState(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
s, err := terraform.ReadState(r)
|
s, err := terraform.ReadState(bytes.NewReader(r))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -116,7 +128,7 @@ func TestPersist(t *testing.T) {
|
||||||
AuthToken: "foobar",
|
AuthToken: "foobar",
|
||||||
}
|
}
|
||||||
blank, _ := blankState(remote)
|
blank, _ := blankState(remote)
|
||||||
if err := Persist(blank); err != nil {
|
if err := Persist(bytes.NewReader(blank)); err != nil {
|
||||||
t.Fatalf("err: %v", err)
|
t.Fatalf("err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue