remote: Supporting push state

This commit is contained in:
Armon Dadgar 2014-10-08 10:28:47 -07:00 committed by Mitchell Hashimoto
parent d1e41bc992
commit bec1dfcd68
4 changed files with 171 additions and 0 deletions

View File

@ -19,6 +19,10 @@ var (
// 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")
@ -187,6 +191,8 @@ func (r *remoteStateClient) PutState(state []byte, force bool) error {
return nil
case http.StatusConflict:
return ErrConflict
case http.StatusPreconditionFailed:
return ErrServerNewer
case http.StatusUnauthorized:
return ErrRequireAuth
case http.StatusForbidden:

View File

@ -193,6 +193,13 @@ func TestPutState(t *testing.T) {
ExpectMD5: hash,
ExpectErr: ErrConflict.Error(),
},
&tcase{
Code: http.StatusPreconditionFailed,
Path: "/foobar",
Body: inp,
ExpectMD5: hash,
ExpectErr: ErrServerNewer.Error(),
},
&tcase{
Code: http.StatusUnauthorized,
Path: "/foobar",

View File

@ -115,6 +115,24 @@ func (sc StateChangeResult) SuccessfulPull() bool {
}
}
// 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 {
@ -312,6 +330,45 @@ func RefreshState(conf *terraform.RemoteState) (StateChangeResult, error) {
panic("Unhandled remote update case")
}
// 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) {
// 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)
}
// 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 := &remoteStateClient{conf: conf}
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
}
}
// blankState is used to return a serialized form of a blank state
// with only the remote info.
func blankState(conf *terraform.RemoteState) ([]byte, error) {

View File

@ -241,6 +241,93 @@ func TestRefreshState_Conflict(t *testing.T) {
}
}
func TestPushState_NoState(t *testing.T) {
defer fixDir(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 fixDir(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 fixDir(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 fixDir(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 fixDir(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 TestBlankState(t *testing.T) {
remote := &terraform.RemoteState{
Name: "foo",
@ -337,6 +424,20 @@ func testRemote(t *testing.T, s *terraform.State) (*terraform.RemoteState, *http
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{
Name: "foo",
Server: 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) {